diff --git a/docs/api/db/ActiveRecord.md b/docs/api/db/ActiveRecord.md index da281d8..822c548 100644 --- a/docs/api/db/ActiveRecord.md +++ b/docs/api/db/ActiveRecord.md @@ -300,7 +300,7 @@ foreach ($customers as $customer) { ~~~ How many SQL queries will be performed in the above code, assuming there are more than 100 customers in -the database? 101! The first SQL query brings back 100 customers. Then for each customer, another SQL query +the database? 101! The first SQL query brings back 100 customers. Then for each customer, a SQL query is performed to bring back the customer's orders. To solve the above performance problem, you can use the so-called *eager loading* by calling [[ActiveQuery::with()]]: @@ -318,7 +318,7 @@ foreach ($customers as $customer) { } ~~~ -As you can see, only two SQL queries were needed for the same task. +As you can see, only two SQL queries are needed for the same task. Sometimes, you may want to customize the relational queries on the fly. It can be diff --git a/docs/autoloader.md b/docs/autoloader.md new file mode 100644 index 0000000..b7696d7 --- /dev/null +++ b/docs/autoloader.md @@ -0,0 +1,19 @@ +Yii2 class loader +================= + +Yii 2 class loader is PSR-0 compliant. That means it can handle most of the PHP +libraries and frameworks out there. + +In order to autoload a library you need to set a root alias for it. + +PEAR-style libraries +-------------------- + +```php +\Yii::setAlias('@Twig', '@app/vendors/Twig'); +``` + +References +---------- + +- YiiBase::autoload \ No newline at end of file diff --git a/docs/code_style.md b/docs/code_style.md index dfa475e..92a934b 100644 --- a/docs/code_style.md +++ b/docs/code_style.md @@ -204,7 +204,7 @@ doIt('a', array( ~~~ if ($event === null) { - return new Event($this); + return new Event(); } elseif ($event instanceof CoolEvent) { return $event->instance(); } else { @@ -251,10 +251,8 @@ switch ($this->phpType) { ~~~ <?php /** - * Component class file. - * * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ ~~~ diff --git a/framework/YiiBase.php b/framework/YiiBase.php index d01648c..16e237d 100644 --- a/framework/YiiBase.php +++ b/framework/YiiBase.php @@ -1,14 +1,12 @@ <?php /** - * YiiBase class file. - * * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ - use yii\base\Exception; use yii\base\InvalidConfigException; +use yii\base\InvalidParamException; use yii\logging\Logger; /** @@ -63,9 +61,9 @@ class YiiBase */ public static $classPath = array(); /** - * @var yii\base\Application the application instance + * @var yii\console\Application|yii\web\Application the application instance */ - public static $application; + public static $app; /** * @var array registered path aliases * @see getAlias @@ -96,7 +94,7 @@ class YiiBase */ public static $objectConfig = array(); - private static $_imported = array(); // alias => class name or directory + private static $_imported = array(); // alias => class name or directory private static $_logger; /** @@ -125,8 +123,8 @@ class YiiBase * * To import a class or a directory, one can use either path alias or class name (can be namespaced): * - * - `@application/components/GoogleMap`: importing the `GoogleMap` class with a path alias; - * - `@application/components/*`: importing the whole `components` directory with a path alias; + * - `@app/components/GoogleMap`: importing the `GoogleMap` class with a path alias; + * - `@app/components/*`: importing the whole `components` directory with a path alias; * - `GoogleMap`: importing the `GoogleMap` class with a class name. [[autoload()]] will be used * when this class is used for the first time. * @@ -161,9 +159,7 @@ class YiiBase return self::$_imported[$alias] = $className; } - if (($path = static::getAlias(dirname($alias))) === false) { - throw new Exception('Invalid path alias: ' . $alias); - } + $path = static::getAlias(dirname($alias)); if ($isClass) { if ($forceInclude) { @@ -193,24 +189,30 @@ class YiiBase * * Note, this method does not ensure the existence of the resulting path. * @param string $alias alias + * @param boolean $throwException whether to throw an exception if the given alias is invalid. + * If this is false and an invalid alias is given, false will be returned by this method. * @return string|boolean path corresponding to the alias, false if the root alias is not previously registered. * @see setAlias */ - public static function getAlias($alias) + public static function getAlias($alias, $throwException = true) { - if (!is_string($alias)) { - return false; - } elseif (isset(self::$aliases[$alias])) { - return self::$aliases[$alias]; - } elseif ($alias === '' || $alias[0] !== '@') { // not an alias - return $alias; - } elseif (($pos = strpos($alias, '/')) !== false) { - $rootAlias = substr($alias, 0, $pos); - if (isset(self::$aliases[$rootAlias])) { - return self::$aliases[$alias] = self::$aliases[$rootAlias] . substr($alias, $pos); + if (is_string($alias)) { + if (isset(self::$aliases[$alias])) { + return self::$aliases[$alias]; + } elseif ($alias === '' || $alias[0] !== '@') { // not an alias + return $alias; + } elseif (($pos = strpos($alias, '/')) !== false || ($pos = strpos($alias, '\\')) !== false) { + $rootAlias = substr($alias, 0, $pos); + if (isset(self::$aliases[$rootAlias])) { + return self::$aliases[$alias] = self::$aliases[$rootAlias] . substr($alias, $pos); + } } } - return false; + if ($throwException) { + throw new InvalidParamException("Invalid path alias: $alias"); + } else { + return false; + } } /** @@ -238,10 +240,8 @@ class YiiBase unset(self::$aliases[$alias]); } elseif ($path[0] !== '@') { self::$aliases[$alias] = rtrim($path, '\\/'); - } elseif (($p = static::getAlias($path)) !== false) { - self::$aliases[$alias] = $p; } else { - throw new Exception('Invalid path: ' . $path); + self::$aliases[$alias] = static::getAlias($path); } } @@ -262,6 +262,7 @@ class YiiBase * * @param string $className class name * @return boolean whether the class has been loaded successfully + * @throws Exception if the class file does not exist */ public static function autoload($className) { @@ -274,14 +275,14 @@ class YiiBase // namespaced class, e.g. yii\base\Component // convert namespace to path alias, e.g. yii\base\Component to @yii/base/Component $alias = '@' . str_replace('\\', '/', ltrim($className, '\\')); - if (($path = static::getAlias($alias)) !== false) { + if (($path = static::getAlias($alias, false)) !== false) { $classFile = $path . '.php'; } } elseif (($pos = strpos($className, '_')) !== false) { // PEAR-styled class, e.g. PHPUnit_Framework_TestCase // convert class name to path alias, e.g. PHPUnit_Framework_TestCase to @PHPUnit/Framework/TestCase $alias = '@' . str_replace('_', '/', $className); - if (($path = static::getAlias($alias)) !== false) { + if (($path = static::getAlias($alias, false)) !== false) { $classFile = $path . '.php'; } } @@ -297,7 +298,7 @@ class YiiBase } } - if (isset($classFile, $alias)) { + if (isset($classFile, $alias) && is_file($classFile)) { if (!YII_DEBUG || basename(realpath($classFile)) === basename($alias) . '.php') { include($classFile); return true; @@ -322,12 +323,12 @@ class YiiBase * the class. For example, * * - `\app\components\GoogleMap`: fully-qualified namespaced class. - * - `@application/components/GoogleMap`: an alias + * - `@app/components/GoogleMap`: an alias * * Below are some usage examples: * * ~~~ - * $object = \Yii::createObject('@application/components/GoogleMap'); + * $object = \Yii::createObject('@app/components/GoogleMap'); * $object = \Yii::createObject(array( * 'class' => '\app\components\GoogleMap', * 'apiKey' => 'xyz', @@ -507,9 +508,6 @@ class YiiBase * i.e., the message returned will be chosen from a few candidates according to the given * number value. This feature is mainly used to solve plural format issue in case * a message has different plural forms in some languages. - * @param string $category message category. Please use only word letters. Note, category 'yii' is - * reserved for Yii framework core code use. See {@link CPhpMessageSource} for - * more interpretation about message category. * @param string $message the original message * @param array $params parameters to be applied to the message using <code>strtr</code>. * The first parameter can be a number without key. @@ -517,62 +515,12 @@ class YiiBase * an appropriate message translation. * You can pass parameter for {@link CChoiceFormat::format} * or plural forms format without wrapping it with array. - * @param string $source which message source application component to use. - * Defaults to null, meaning using 'coreMessages' for messages belonging to - * the 'yii' category and using 'messages' for the rest messages. * @param string $language the target language. If null (default), the {@link CApplication::getLanguage application language} will be used. * @return string the translated message * @see CMessageSource */ - public static function t($category, $message, $params = array(), $source = null, $language = null) + public static function t($message, $params = array(), $language = null) { - // todo; - return $params !== array() ? strtr($message, $params) : $message; - if (self::$application !== null) - { - if ($source === null) - { - $source = $category === 'yii' ? 'coreMessages' : 'messages'; - } - if (($source = self::$application->getComponent($source)) !== null) - { - $message = $source->translate($category, $message, $language); - } - } - if ($params === array()) - { - return $message; - } - if (!is_array($params)) - { - $params = array($params); - } - if (isset($params[0])) // number choice - { - if (strpos($message, '|') !== false) - { - if (strpos($message, '#') === false) - { - $chunks = explode('|', $message); - $expressions = self::$application->getLocale($language)->getPluralRules(); - if ($n = min(count($chunks), count($expressions))) - { - for ($i = 0; $i < $n; $i++) - { - $chunks[$i] = $expressions[$i] . '#' . $chunks[$i]; - } - - $message = implode('|', $chunks); - } - } - $message = CChoiceFormat::format($message, $params[0]); - } - if (!isset($params['{n}'])) - { - $params['{n}'] = $params[0]; - } - unset($params[0]); - } - return $params !== array() ? strtr($message, $params) : $message; + return Yii::$app->getI18N()->translate($message, $params, $language); } } diff --git a/framework/base/Action.php b/framework/base/Action.php index 8d4ec5a..7142539 100644 --- a/framework/base/Action.php +++ b/framework/base/Action.php @@ -1,9 +1,7 @@ <?php /** - * Action class file. - * * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ @@ -56,6 +54,15 @@ class Action extends Component } /** + * Returns the unique ID of this action among the whole application. + * @return string the unique ID of this action among the whole application. + */ + public function getUniqueId() + { + return $this->controller->getUniqueId() . '/' . $this->id; + } + + /** * Runs this action with the specified parameters. * This method is mainly invoked by the controller. * @param array $params the parameters to be bound to the action's run() method. @@ -67,36 +74,7 @@ class Action extends Component if (!method_exists($this, 'run')) { throw new InvalidConfigException(get_class($this) . ' must define a "run()" method.'); } - $method = new \ReflectionMethod($this, 'run'); - $args = $this->bindActionParams($method, $params); - return (int)$method->invokeArgs($this, $args); - } - - /** - * Binds the given parameters to the action method. - * The returned array contains the parameters that need to be passed to the action method. - * This method calls [[Controller::validateActionParams()]] to check if any exception - * should be raised if there are missing or unknown parameters. - * @param \ReflectionMethod $method the action method reflection object - * @param array $params the supplied parameters - * @return array the parameters that can be passed to the action method - */ - protected function bindActionParams($method, $params) - { - $args = array(); - $missing = array(); - foreach ($method->getParameters() as $param) { - $name = $param->getName(); - if (array_key_exists($name, $params)) { - $args[] = $params[$name]; - unset($params[$name]); - } elseif ($param->isDefaultValueAvailable()) { - $args[] = $param->getDefaultValue(); - } else { - $missing[] = $name; - } - } - $this->controller->validateActionParams($this, $missing, $params); - return $args; + $args = $this->controller->bindActionParams($this, $params); + return (int)call_user_func_array(array($this, 'run'), $args); } } diff --git a/framework/base/ActionEvent.php b/framework/base/ActionEvent.php index ee945a8..7c5a40c 100644 --- a/framework/base/ActionEvent.php +++ b/framework/base/ActionEvent.php @@ -1,9 +1,7 @@ <?php /** - * ActionEvent class file. - * * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ diff --git a/framework/base/ActionFilter.php b/framework/base/ActionFilter.php new file mode 100644 index 0000000..1f82e5d --- /dev/null +++ b/framework/base/ActionFilter.php @@ -0,0 +1,90 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\base; + +/** + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class ActionFilter extends Behavior +{ + /** + * @var array list of action IDs that this filter should apply to. If this property is not set, + * then the filter applies to all actions, unless they are listed in [[except]]. + */ + public $only; + /** + * @var array list of action IDs that this filter should not apply to. + */ + public $except = array(); + + /** + * Declares event handlers for the [[owner]]'s events. + * @return array events (array keys) and the corresponding event handler methods (array values). + */ + public function events() + { + return array( + 'beforeAction' => 'beforeFilter', + 'afterAction' => 'afterFilter', + ); + } + + /** + * @param ActionEvent $event + * @return boolean + */ + public function beforeFilter($event) + { + if ($this->isActive($event->action)) { + $event->isValid = $this->beforeAction($event->action); + } + return $event->isValid; + } + + /** + * @param ActionEvent $event + * @return boolean + */ + public function afterFilter($event) + { + if ($this->isActive($event->action)) { + $this->afterAction($event->action); + } + } + + /** + * 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) + { + return true; + } + + /** + * This method is invoked right after an action is executed. + * You may override this method to do some postprocessing for the action. + * @param Action $action the action just executed. + */ + public function afterAction($action) + { + } + + /** + * Returns a value indicating whether the filer is active for the given action. + * @param Action $action the action being filtered + * @return boolean whether the filer is active for the given action. + */ + protected function isActive($action) + { + return !in_array($action->id, $this->except, true) && (empty($this->only) || in_array($action->id, $this->only, true)); + } +} \ No newline at end of file diff --git a/framework/base/Application.php b/framework/base/Application.php index 40e8437..9be1939 100644 --- a/framework/base/Application.php +++ b/framework/base/Application.php @@ -1,16 +1,14 @@ <?php /** - * Application class file. - * * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ namespace yii\base; use Yii; -use yii\util\FileHelper; +use yii\helpers\FileHelper; /** * Application is the base class for all application classes. @@ -30,10 +28,6 @@ use yii\util\FileHelper; * persistence method. This application component is dynamically loaded when needed.</li> * <li>{@link getCache cache}: provides caching feature. This application component is * disabled by default.</li> - * <li>{@link getMessages messages}: provides the message source for translating - * application messages. This application component is dynamically loaded when needed.</li> - * <li>{@link getCoreMessages coreMessages}: provides the message source for translating - * Yii framework messages. This application component is dynamically loaded when needed.</li> * </ul> * * Application will undergo the following life cycles when processing a user request: @@ -57,29 +51,34 @@ class Application extends Module const EVENT_BEFORE_REQUEST = 'beforeRequest'; const EVENT_AFTER_REQUEST = 'afterRequest'; /** - * @var string the application name. Defaults to 'My Application'. + * @var string the application name. */ public $name = 'My Application'; /** - * @var string the version of this application. Defaults to '1.0'. + * @var string the version of this application. */ public $version = '1.0'; /** - * @var string the charset currently used for the application. Defaults to 'UTF-8'. + * @var string the charset currently used for the application. */ public $charset = 'UTF-8'; /** + * @var string the language that is meant to be used for end users. + * @see sourceLanguage + */ + public $language = 'en_US'; + /** * @var string the language that the application is written in. This mainly refers to - * the language that the messages and view files are in. Defaults to 'en_us' (US English). + * the language that the messages and view files are written in. * @see language */ - public $sourceLanguage = 'en_us'; + public $sourceLanguage = 'en_US'; /** * @var array IDs of the components that need to be loaded when the application starts. */ public $preload = array(); /** - * @var Controller the currently active controller instance + * @var \yii\web\Controller|\yii\console\Controller the currently active controller instance */ public $controller; /** @@ -93,7 +92,12 @@ class Application extends Module private $_runtimePath; private $_ended = false; - private $_language; + + /** + * @var string Used to reserve memory for fatal error handler. This memory + * reserve can be removed if it's OK to write to PHP log only in this particular case. + */ + private $_memoryReserve; /** * Constructor. @@ -104,11 +108,12 @@ class Application extends Module */ public function __construct($id, $basePath, $config = array()) { - Yii::$application = $this; + Yii::$app = $this; $this->id = $id; $this->setBasePath($basePath); if (YII_ENABLE_ERROR_HANDLER) { + ini_set('display_errors', 0); set_exception_handler(array($this, 'handleException')); set_error_handler(array($this, 'handleError'), error_reporting()); } @@ -141,12 +146,64 @@ class Application extends Module $this->_ended = true; $this->afterRequest(); } + + $this->handleFatalError(); + if ($exit) { exit($status); } } /** + * Handles fatal PHP errors + */ + public function handleFatalError() + { + if (YII_ENABLE_ERROR_HANDLER) { + $error = error_get_last(); + + if (ErrorException::isFatalErorr($error)) { + unset($this->_memoryReserve); + $exception = new ErrorException($error['message'], $error['type'], $error['type'], $error['file'], $error['line']); + + if (function_exists('xdebug_get_function_stack')) { + $trace = array_slice(array_reverse(xdebug_get_function_stack()), 4, -1); + foreach ($trace as &$frame) { + if (!isset($frame['function'])) { + $frame['function'] = 'unknown'; + } + + // XDebug < 2.1.1: http://bugs.xdebug.org/view.php?id=695 + if (!isset($frame['type'])) { + $frame['type'] = '::'; + } + + // XDebug has a different key name + $frame['args'] = array(); + if (isset($frame['params']) && !isset($frame['args'])) { + $frame['args'] = $frame['params']; + } + } + + $ref = new \ReflectionProperty('Exception', 'trace'); + $ref->setAccessible(true); + $ref->setValue($exception, $trace); + } + + $this->logException($exception); + + if (($handler = $this->getErrorHandler()) !== null) { + @$handler->handle($exception); + } else { + $this->renderException($exception); + } + + exit(1); + } + } + } + + /** * Runs the application. * This is the main entrance of an application. * @return integer the exit status (0 means normal, non-zero values mean abnormal) @@ -154,6 +211,10 @@ class Application extends Module public function run() { $this->beforeRequest(); + // Allocating twice more than required to display memory exhausted error + // in case of trying to allocate last 1 byte while all memory is taken. + $this->_memoryReserve = str_repeat('x', 1024 * 256); + register_shutdown_function(array($this, 'end'), 0, false); $status = $this->processRequest(); $this->afterRequest(); return $status; @@ -213,29 +274,6 @@ class Application extends Module } /** - * Returns the language that the end user is using. - * @return string the language that the user is using (e.g. 'en_US', 'zh_CN'). - * Defaults to the value of [[sourceLanguage]]. - */ - public function getLanguage() - { - return $this->_language === null ? $this->sourceLanguage : $this->_language; - } - - /** - * Specifies which language the end user is using. - * This is the language that the application should use to display to end users. - * By default, [[language]] and [[sourceLanguage]] are the same. - * Do not set this property unless your application needs to support multiple languages. - * @param string $language the user language (e.g. 'en_US', 'zh_CN'). - * If it is null, the [[sourceLanguage]] will be used. - */ - public function setLanguage($language) - { - $this->_language = $language; - } - - /** * Returns the time zone used by this application. * This is a simple wrapper of PHP function date_default_timezone_get(). * @return string the time zone used by this application. @@ -257,14 +295,6 @@ class Application extends Module date_default_timezone_set($value); } - // /** - // * Returns the security manager component. - // * @return SecurityManager the security manager application component. - // */ - // public function getSecurityManager() - // { - // return $this->getComponent('securityManager'); - // } // // /** // * Returns the locale instance. @@ -295,23 +325,6 @@ class Application extends Module // return $this->getLocale()->getDateFormatter(); // } // - // /** - // * Returns the core message translations component. - // * @return \yii\i18n\MessageSource the core message translations - // */ - // public function getCoreMessages() - // { - // return $this->getComponent('coreMessages'); - // } - // - // /** - // * Returns the application message translations component. - // * @return \yii\i18n\MessageSource the application message translations - // */ - // public function getMessages() - // { - // return $this->getComponent('messages'); - // } /** * Returns the database connection component. @@ -332,15 +345,6 @@ class Application extends Module } /** - * Returns the application theme. - * @return Theme the theme that this application is currently using. - */ - public function getTheme() - { - return $this->getComponent('theme'); - } - - /** * Returns the cache component. * @return \yii\caching\Cache the cache application component. Null if the component is not enabled. */ @@ -351,7 +355,7 @@ class Application extends Module /** * Returns the request component. - * @return Request the request component + * @return \yii\web\Request|\yii\console\Request the request component */ public function getRequest() { @@ -359,12 +363,30 @@ class Application extends Module } /** - * Returns the view renderer. - * @return ViewRenderer the view renderer used by this application. + * Returns the view object. + * @return View the view object that is used to render various view files. + */ + public function getView() + { + return $this->getComponent('view'); + } + + /** + * Returns the URL manager for this application. + * @return \yii\web\UrlManager the URL manager for this application. + */ + public function getUrlManager() + { + return $this->getComponent('urlManager'); + } + + /** + * Returns the internationalization (i18n) component + * @return \yii\i18n\I18N the internationalization component */ - public function getViewRenderer() + public function getI18N() { - return $this->getComponent('viewRenderer'); + return $this->getComponent('i18n'); } /** @@ -372,9 +394,7 @@ class Application extends Module */ public function registerDefaultAliases() { - Yii::$aliases['@application'] = $this->getBasePath(); - Yii::$aliases['@entry'] = dirname($_SERVER['SCRIPT_FILENAME']); - Yii::$aliases['@www'] = ''; + Yii::$aliases['@app'] = $this->getBasePath(); } /** @@ -387,20 +407,15 @@ class Application extends Module 'errorHandler' => array( 'class' => 'yii\base\ErrorHandler', ), - 'coreMessages' => array( - 'class' => 'yii\i18n\PhpMessageSource', - 'language' => 'en_us', - 'basePath' => '@yii/messages', - ), - 'messages' => array( - 'class' => 'yii\i18n\PhpMessageSource', - ), - 'securityManager' => array( - 'class' => 'yii\base\SecurityManager', + 'i18n' => array( + 'class' => 'yii\i18n\I18N', ), 'urlManager' => array( 'class' => 'yii\web\UrlManager', ), + 'view' => array( + 'class' => 'yii\base\View', + ), )); } @@ -413,12 +428,24 @@ class Application extends Module * @param string $message the error message * @param string $file the filename that the error was raised in * @param integer $line the line number the error was raised at - * @throws \ErrorException the error exception + * + * @throws ErrorException */ public function handleError($code, $message, $file, $line) { if (error_reporting() !== 0) { - throw new \ErrorException($message, 0, $code, $file, $line); + $exception = new ErrorException($message, $code, $code, $file, $line); + + // in case error appeared in __toString method we can't throw any exception + $trace = debug_backtrace(false); + array_shift($trace); + foreach ($trace as $frame) { + if ($frame['function'] == '__toString') { + $this->handleException($exception); + } + } + + throw $exception; } } @@ -447,11 +474,14 @@ class Application extends Module $this->end(1); - } catch(\Exception $e) { + } catch (\Exception $e) { // exception could be thrown in end() or ErrorHandler::handle() $msg = (string)$e; $msg .= "\nPrevious exception:\n"; $msg .= (string)$exception; + if (YII_DEBUG) { + echo $msg; + } $msg .= "\n\$_SERVER = " . var_export($_SERVER, true); error_log($msg); exit(1); @@ -464,7 +494,7 @@ class Application extends Module */ public function renderException($exception) { - if ($exception instanceof Exception && ($exception->causedByUser || !YII_DEBUG)) { + if ($exception instanceof Exception && ($exception instanceof UserException || !YII_DEBUG)) { $message = $exception->getName() . ': ' . $exception->getMessage(); } else { $message = YII_DEBUG ? (string)$exception : 'Error: ' . $exception->getMessage(); diff --git a/framework/base/Behavior.php b/framework/base/Behavior.php index 9155097..abe08bb 100644 --- a/framework/base/Behavior.php +++ b/framework/base/Behavior.php @@ -1,9 +1,7 @@ <?php /** - * Behavior class file. - * * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ diff --git a/framework/base/Component.php b/framework/base/Component.php index c6c91eb..f1d549b 100644 --- a/framework/base/Component.php +++ b/framework/base/Component.php @@ -1,9 +1,7 @@ <?php /** - * Component class file. - * * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ @@ -60,7 +58,7 @@ class Component extends \yii\base\Object } } } - throw new UnknownPropertyException('Getting unknown property: ' . get_class($this) . '.' . $name); + throw new UnknownPropertyException('Getting unknown property: ' . get_class($this) . '::' . $name); } /** @@ -107,9 +105,9 @@ class Component extends \yii\base\Object } } if (method_exists($this, 'get' . $name)) { - throw new InvalidCallException('Setting read-only property: ' . get_class($this) . '.' . $name); + throw new InvalidCallException('Setting read-only property: ' . get_class($this) . '::' . $name); } else { - throw new UnknownPropertyException('Setting unknown property: ' . get_class($this) . '.' . $name); + throw new UnknownPropertyException('Setting unknown property: ' . get_class($this) . '::' . $name); } } @@ -424,7 +422,10 @@ class Component extends \yii\base\Object $this->ensureBehaviors(); if (isset($this->_e[$name]) && $this->_e[$name]->getCount()) { if ($event === null) { - $event = new Event($this); + $event = new Event; + } + if ($event->sender === null) { + $event->sender = $this; } $event->handled = false; $event->name = $name; diff --git a/framework/base/Controller.php b/framework/base/Controller.php index 804b339..ff6d8f7 100644 --- a/framework/base/Controller.php +++ b/framework/base/Controller.php @@ -1,23 +1,19 @@ <?php /** - * Controller class file. - * * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ namespace yii\base; use Yii; -use yii\util\StringHelper; +use yii\helpers\FileHelper; +use yii\helpers\StringHelper; /** * Controller is the base class for classes containing controller logic. * - * @property string $route the route (module ID, controller ID and action ID) of the current request. - * @property string $uniqueId the controller ID that is prefixed with the module ID (if any). - * * @author Qiang Xue <qiang.xue@gmail.com> * @since 2.0 */ @@ -72,9 +68,9 @@ class Controller extends Component * * ~~~ * return array( - * 'action1' => '@application/components/Action1', + * 'action1' => '@app/components/Action1', * 'action2' => array( - * 'class' => '@application/components/Action2', + * 'class' => '@app/components/Action2', * 'property1' => 'value1', * 'property2' => 'value2', * ), @@ -139,8 +135,50 @@ class Controller extends Component } elseif ($pos > 0) { return $this->module->runAction($route, $params); } else { - return \Yii::$application->runAction(ltrim($route, '/'), $params); + return \Yii::$app->runAction(ltrim($route, '/'), $params); + } + } + + /** + * Binds the parameters to the action. + * This method is invoked by [[Action]] when it begins to run with the given parameters. + * This method will check the parameter names that the action requires and return + * the provided parameters according to the requirement. If there is any missing parameter, + * an exception will be thrown. + * @param Action $action the action to be bound with parameters + * @param array $params the parameters to be bound to the action + * @return array the valid parameters that the action can run with. + * @throws InvalidRequestException if there are missing parameters. + */ + public function bindActionParams($action, $params) + { + if ($action instanceof InlineAction) { + $method = new \ReflectionMethod($this, $action->actionMethod); + } else { + $method = new \ReflectionMethod($action, 'run'); + } + + $args = array(); + $missing = array(); + foreach ($method->getParameters() as $param) { + $name = $param->getName(); + if (array_key_exists($name, $params)) { + $args[] = $params[$name]; + unset($params[$name]); + } elseif ($param->isDefaultValueAvailable()) { + $args[] = $param->getDefaultValue(); + } else { + $missing[] = $name; + } + } + + if ($missing !== array()) { + throw new InvalidRequestException(Yii::t('yii|Missing required parameters: {params}', array( + '{params}' => implode(', ', $missing), + ))); } + + return $args; } /** @@ -250,34 +288,51 @@ class Controller extends Component */ public function getRoute() { - return $this->action !== null ? $this->getUniqueId() . '/' . $this->action->id : $this->getUniqueId(); + return $this->action !== null ? $this->action->getUniqueId() : $this->getUniqueId(); } /** * Renders a view and applies layout if available. - * - * @param $view - * @param array $params - * @return string + * @param string $view the view name. Please refer to [[findViewFile()]] on how to specify a view name. + * @param array $params the parameters (name-value pairs) that should be made available in the view. + * These parameters will not be available in the layout. + * @return string the rendering result. + * @throws InvalidParamException if the view file or the layout file does not exist. */ public function render($view, $params = array()) { - return $this->createView()->render($view, $params); - } - - public function renderContent($content) - { - return $this->createView()->renderContent($content); + $output = Yii::$app->getView()->render($view, $params, $this); + $layoutFile = $this->findLayoutFile(); + if ($layoutFile !== false) { + return Yii::$app->getView()->renderFile($layoutFile, array('content' => $output), $this); + } else { + return $output; + } } + /** + * Renders a view. + * This method differs from [[render()]] in that it does not apply any layout. + * @param string $view the view name. Please refer to [[findViewFile()]] on how to specify a view name. + * @param array $params the parameters (name-value pairs) that should be made available in the view. + * @return string the rendering result. + * @throws InvalidParamException if the view file does not exist. + */ public function renderPartial($view, $params = array()) { - return $this->createView()->renderPartial($view, $params); + return Yii::$app->getView()->render($view, $params, $this); } - public function createView() + /** + * Renders a view file. + * @param string $file the view file to be rendered. This can be either a file path or a path alias. + * @param array $params the parameters (name-value pairs) that should be made available in the view. + * @return string the rendering result. + * @throws InvalidParamException if the view file does not exist. + */ + public function renderFile($file, $params = array()) { - return new View($this); + return Yii::$app->getView()->renderFile($file, $params, $this); } /** @@ -290,4 +345,63 @@ class Controller extends Component { return $this->module->getViewPath() . DIRECTORY_SEPARATOR . $this->id; } + + /** + * Finds the applicable layout file. + * + * This method locates an applicable layout file via two steps. + * + * In the first step, it determines the layout name and the context module: + * + * - If [[layout]] is specified as a string, use it as the layout name and [[module]] as the context module; + * - If [[layout]] is null, search through all ancestor modules of this controller and find the first + * module whose [[Module::layout|layout]] is not null. The layout and the corresponding module + * are used as the layout name and the context module, respectively. If such a module is not found + * or the corresponding layout is not a string, it will return false, meaning no applicable layout. + * + * In the second step, it determines the actual layout file according to the previously found layout name + * and context module. The layout name can be + * + * - a path alias (e.g. "@app/views/layouts/main"); + * - an absolute path (e.g. "/main"): the layout name starts with a slash. The actual layout file will be + * looked for under the [[Application::layoutPath|layout path]] of the application; + * - a relative path (e.g. "main"): the actual layout layout file will be looked for under the + * [[Module::viewPath|view path]] of the context module. + * + * If the layout name does not contain a file extension, it will use the default one `.php`. + * + * @return string|boolean the layout file path, or false if layout is not needed. + * @throws InvalidParamException if an invalid path alias is used to specify the layout + */ + protected function findLayoutFile() + { + $module = $this->module; + if (is_string($this->layout)) { + $view = $this->layout; + } elseif ($this->layout === null) { + while ($module !== null && $module->layout === null) { + $module = $module->module; + } + if ($module !== null && is_string($module->layout)) { + $view = $module->layout; + } + } + + if (!isset($view)) { + return false; + } + + if (strncmp($view, '@', 1) === 0) { + $file = Yii::getAlias($view); + } elseif (strncmp($view, '/', 1) === 0) { + $file = Yii::$app->getLayoutPath() . DIRECTORY_SEPARATOR . $view; + } else { + $file = $module->getLayoutPath() . DIRECTORY_SEPARATOR . $view; + } + + if (FileHelper::getExtension($file) === '') { + $file .= '.php'; + } + return $file; + } } diff --git a/framework/base/Dictionary.php b/framework/base/Dictionary.php index cc61886..52262cb 100644 --- a/framework/base/Dictionary.php +++ b/framework/base/Dictionary.php @@ -1,15 +1,13 @@ <?php /** - * Dictionary class file. - * * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ namespace yii\base; -use yii\util\ArrayHelper; +use yii\helpers\ArrayHelper; /** * Dictionary implements a collection that stores key-value pairs. @@ -150,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) { @@ -166,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); } @@ -184,13 +182,13 @@ class Dictionary extends Object implements \IteratorAggregate, \ArrayAccess, \Co * Copies iterable data into the dictionary. * Note, existing data in the dictionary will be cleared first. * @param mixed $data the data to be copied from, must be an array or an object implementing `Traversable` - * @throws InvalidCallException if data is neither an array nor an iterator. + * @throws InvalidParamException if data is neither an array nor an iterator. */ public function copyFrom($data) { if (is_array($data) || $data instanceof \Traversable) { if ($this->_d !== array()) { - $this->clear(); + $this->removeAll(); } if ($data instanceof self) { $data = $data->_d; @@ -199,7 +197,7 @@ class Dictionary extends Object implements \IteratorAggregate, \ArrayAccess, \Co $this->add($key, $value); } } else { - throw new InvalidCallException('Data must be either an array or an object implementing Traversable.'); + throw new InvalidParamException('Data must be either an array or an object implementing Traversable.'); } } @@ -216,7 +214,7 @@ class Dictionary extends Object implements \IteratorAggregate, \ArrayAccess, \Co * * @param array|\Traversable $data the data to be merged with. It must be an array or object implementing Traversable * @param boolean $recursive whether the merging should be recursive. - * @throws InvalidCallException if data is neither an array nor an object implementing `Traversable`. + * @throws InvalidParamException if data is neither an array nor an object implementing `Traversable`. */ public function mergeWith($data, $recursive = true) { @@ -240,7 +238,7 @@ class Dictionary extends Object implements \IteratorAggregate, \ArrayAccess, \Co } } } else { - throw new InvalidCallException('The data to be merged with must be an array or an object implementing Traversable.'); + throw new InvalidParamException('The data to be merged with must be an array or an object implementing Traversable.'); } } @@ -254,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/DictionaryIterator.php b/framework/base/DictionaryIterator.php index 61f61cf..0d15bb0 100644 --- a/framework/base/DictionaryIterator.php +++ b/framework/base/DictionaryIterator.php @@ -1,9 +1,7 @@ <?php /** - * DictionaryIterator class file. - * * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ diff --git a/framework/base/ErrorException.php b/framework/base/ErrorException.php new file mode 100644 index 0000000..465d839 --- /dev/null +++ b/framework/base/ErrorException.php @@ -0,0 +1,81 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\base; + +/** + * ErrorException represents a PHP error. + * + * @author Alexander Makarov <sam@rmcreative.ru> + * @since 2.0 + */ +class ErrorException extends Exception +{ + protected $severity; + + /** + * Constructs the exception + * @link http://php.net/manual/en/errorexception.construct.php + * @param $message [optional] + * @param $code [optional] + * @param $severity [optional] + * @param $filename [optional] + * @param $lineno [optional] + * @param $previous [optional] + */ + public function __construct($message = '', $code = 0, $severity = 1, $filename = __FILE__, $lineno = __LINE__, \Exception $previous = null) + { + parent::__construct($message, $code, $previous); + $this->severity = $severity; + $this->file = $filename; + $this->line = $lineno; + } + + /** + * Gets the exception severity + * @link http://php.net/manual/en/errorexception.getseverity.php + * @return int the severity level of the exception. + */ + final public function getSeverity() + { + return $this->severity; + } + + /** + * Returns if error is one of fatal type + * + * @param array $error error got from error_get_last() + * @return bool if error is one of fatal type + */ + public static function isFatalErorr($error) + { + return isset($error['type']) && in_array($error['type'], array(E_ERROR, E_PARSE, E_CORE_ERROR, E_CORE_WARNING, E_COMPILE_ERROR, E_COMPILE_WARNING)); + } + + /** + * @return string the user-friendly name of this exception + */ + public function getName() + { + $names = array( + E_ERROR => \Yii::t('yii|Fatal Error'), + E_PARSE => \Yii::t('yii|Parse Error'), + E_CORE_ERROR => \Yii::t('yii|Core Error'), + E_COMPILE_ERROR => \Yii::t('yii|Compile Error'), + E_USER_ERROR => \Yii::t('yii|User Error'), + E_WARNING => \Yii::t('yii|Warning'), + E_CORE_WARNING => \Yii::t('yii|Core Warning'), + E_COMPILE_WARNING => \Yii::t('yii|Compile Warning'), + E_USER_WARNING => \Yii::t('yii|User Warning'), + E_STRICT => \Yii::t('yii|Strict'), + E_NOTICE => \Yii::t('yii|Notice'), + E_RECOVERABLE_ERROR => \Yii::t('yii|Recoverable Error'), + E_DEPRECATED => \Yii::t('yii|Deprecated'), + ); + return isset($names[$this->getCode()]) ? $names[$this->getCode()] : \Yii::t('yii|Error'); + } +} diff --git a/framework/base/ErrorHandler.php b/framework/base/ErrorHandler.php index 0b6bf97..f71b8c8 100644 --- a/framework/base/ErrorHandler.php +++ b/framework/base/ErrorHandler.php @@ -1,9 +1,7 @@ <?php /** - * ErrorHandler class file. - * * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ @@ -18,7 +16,7 @@ namespace yii\base; * @author Qiang Xue <qiang.xue@gmail.com> * @since 2.0 */ -use yii\util\VarDumper; +use yii\helpers\VarDumper; class ErrorHandler extends Component { @@ -36,7 +34,7 @@ class ErrorHandler extends Component public $discardExistingOutput = true; /** * @var string the route (eg 'site/error') to the controller action that will be used to display external errors. - * Inside the action, it can retrieve the error information by \Yii::$application->errorHandler->error. + * Inside the action, it can retrieve the error information by \Yii::$app->errorHandler->error. * This property defaults to null, meaning ErrorHandler will handle the error display. */ public $errorAction; @@ -71,27 +69,27 @@ class ErrorHandler extends Component protected function render($exception) { if ($this->errorAction !== null) { - \Yii::$application->runAction($this->errorAction); - } elseif (\Yii::$application instanceof \yii\web\Application) { + \Yii::$app->runAction($this->errorAction); + } elseif (\Yii::$app instanceof \yii\web\Application) { if (!headers_sent()) { $errorCode = $exception instanceof HttpException ? $exception->statusCode : 500; header("HTTP/1.0 $errorCode " . get_class($exception)); } if (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] === 'XMLHttpRequest') { - \Yii::$application->renderException($exception); + \Yii::$app->renderException($exception); } else { - $view = new View($this); - if (!YII_DEBUG || $exception instanceof Exception && $exception->causedByUser) { + $view = new View; + if (!YII_DEBUG || $exception instanceof UserException) { $viewName = $this->errorView; } else { $viewName = $this->exceptionView; } echo $view->render($viewName, array( 'exception' => $exception, - )); + ), $this); } } else { - \Yii::$application->renderException($exception); + \Yii::$app->renderException($exception); } } @@ -239,7 +237,7 @@ class ErrorHandler extends Component public function htmlEncode($text) { - return htmlspecialchars($text, ENT_QUOTES, \Yii::$application->charset); + return htmlspecialchars($text, ENT_QUOTES, \Yii::$app->charset); } public function clearOutput() @@ -255,15 +253,10 @@ class ErrorHandler extends Component */ public function renderAsHtml($exception) { - $view = new View($this); - if (!YII_DEBUG || $exception instanceof Exception && $exception->causedByUser) { - $viewName = $this->errorView; - } else { - $viewName = $this->exceptionView; - } + $view = new View; $name = !YII_DEBUG || $exception instanceof HttpException ? $this->errorView : $this->exceptionView; echo $view->render($name, array( 'exception' => $exception, - )); + ), $this); } } diff --git a/framework/base/Event.php b/framework/base/Event.php index 540e982..b86ed7c 100644 --- a/framework/base/Event.php +++ b/framework/base/Event.php @@ -1,9 +1,7 @@ <?php /** - * Event class file. - * * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ @@ -30,7 +28,8 @@ class Event extends \yii\base\Object */ public $name; /** - * @var object the sender of this event + * @var object the sender of this event. If not set, this property will be + * set as the object whose "trigger()" method is called. */ public $sender; /** @@ -40,21 +39,7 @@ class Event extends \yii\base\Object */ public $handled = false; /** - * @var mixed extra data associated with the event. + * @var mixed extra custom data associated with the event. */ public $data; - - /** - * Constructor. - * - * @param mixed $sender sender of the event - * @param mixed $data extra data associated with the event - * @param array $config name-value pairs that will be used to initialize the object properties - */ - public function __construct($sender = null, $data = null, $config = array()) - { - $this->sender = $sender; - $this->data = $data; - parent::__construct($config); - } } diff --git a/framework/base/Exception.php b/framework/base/Exception.php index ab681e2..9ee698b 100644 --- a/framework/base/Exception.php +++ b/framework/base/Exception.php @@ -1,9 +1,7 @@ <?php /** - * Exception class file. - * * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ @@ -18,16 +16,10 @@ namespace yii\base; class Exception extends \Exception { /** - * @var boolean whether this exception is caused by end user's mistake (e.g. wrong URL) - */ - public $causedByUser = false; - - /** * @return string the user-friendly name of this exception */ public function getName() { - return \Yii::t('yii', 'Exception'); + return \Yii::t('yii|Exception'); } -} - +} \ No newline at end of file diff --git a/framework/base/HttpException.php b/framework/base/HttpException.php index d2de5bc..94a9a55 100644 --- a/framework/base/HttpException.php +++ b/framework/base/HttpException.php @@ -1,9 +1,7 @@ <?php /** - * HttpException class file. - * * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ @@ -19,16 +17,12 @@ namespace yii\base; * @author Qiang Xue <qiang.xue@gmail.com> * @since 2.0 */ -class HttpException extends Exception +class HttpException extends UserException { /** * @var integer HTTP status code, such as 403, 404, 500, etc. */ public $statusCode; - /** - * @var boolean whether this exception is caused by end user's mistake (e.g. wrong URL) - */ - public $causedByUser = true; /** * Constructor. @@ -41,4 +35,73 @@ class HttpException extends Exception $this->statusCode = $status; parent::__construct($message, $code); } + + /** + * @return string the user-friendly name of this exception + */ + public function getName() + { + static $httpCodes = array( + 100 => 'Continue', + 101 => 'Switching Protocols', + 102 => 'Processing', + 118 => 'Connection timed out', + 200 => 'OK', + 201 => 'Created', + 202 => 'Accepted', + 203 => 'Non-Authoritative', + 204 => 'No Content', + 205 => 'Reset Content', + 206 => 'Partial Content', + 207 => 'Multi-Status', + 210 => 'Content Different', + 300 => 'Multiple Choices', + 301 => 'Moved Permanently', + 302 => 'Found', + 303 => 'See Other', + 304 => 'Not Modified', + 305 => 'Use Proxy', + 307 => 'Temporary Redirect', + 310 => 'Too many Redirect', + 400 => 'Bad Request', + 401 => 'Unauthorized', + 402 => 'Payment Required', + 403 => 'Forbidden', + 404 => 'Not Found', + 405 => 'Method Not Allowed', + 406 => 'Not Acceptable', + 407 => 'Proxy Authentication Required', + 408 => 'Request Time-out', + 409 => 'Conflict', + 410 => 'Gone', + 411 => 'Length Required', + 412 => 'Precondition Failed', + 413 => 'Request Entity Too Large', + 414 => 'Request-URI Too Long', + 415 => 'Unsupported Media Type', + 416 => 'Requested range unsatisfiable', + 417 => 'Expectation failed', + 418 => 'I’m a teapot', + 422 => 'Unprocessable entity', + 423 => 'Locked', + 424 => 'Method failure', + 425 => 'Unordered Collection', + 426 => 'Upgrade Required', + 449 => 'Retry With', + 450 => 'Blocked by Windows Parental Controls', + 500 => 'Internal Server Error', + 501 => 'Not Implemented', + 502 => 'Bad Gateway ou Proxy Error', + 503 => 'Service Unavailable', + 504 => 'Gateway Time-out', + 505 => 'HTTP Version not supported', + 507 => 'Insufficient storage', + 509 => 'Bandwidth Limit Exceeded', + ); + + if(isset($httpCodes[$this->statusCode])) + return $httpCodes[$this->statusCode]; + else + return \Yii::t('yii|Error'); + } } diff --git a/framework/base/InlineAction.php b/framework/base/InlineAction.php index 4cd5413..c5afe28 100644 --- a/framework/base/InlineAction.php +++ b/framework/base/InlineAction.php @@ -1,9 +1,7 @@ <?php /** - * InlineAction class file. - * * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ @@ -45,8 +43,7 @@ class InlineAction extends Action */ public function runWithParams($params) { - $method = new \ReflectionMethod($this->controller, $this->actionMethod); - $args = $this->bindActionParams($method, $params); - return (int)$method->invokeArgs($this->controller, $args); + $args = $this->controller->bindActionParams($this, $params); + return (int)call_user_func_array(array($this->controller, $this->actionMethod), $args); } } diff --git a/framework/base/InvalidCallException.php b/framework/base/InvalidCallException.php index a1df021..9aefe14 100644 --- a/framework/base/InvalidCallException.php +++ b/framework/base/InvalidCallException.php @@ -1,9 +1,7 @@ <?php /** - * InvalidCallException class file. - * * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ @@ -15,14 +13,14 @@ namespace yii\base; * @author Qiang Xue <qiang.xue@gmail.com> * @since 2.0 */ -class InvalidCallException extends \Exception +class InvalidCallException extends Exception { /** * @return string the user-friendly name of this exception */ public function getName() { - return \Yii::t('yii', 'Invalid Call'); + return \Yii::t('yii|Invalid Call'); } } diff --git a/framework/base/InvalidConfigException.php b/framework/base/InvalidConfigException.php index 3c100d1..389737c 100644 --- a/framework/base/InvalidConfigException.php +++ b/framework/base/InvalidConfigException.php @@ -1,9 +1,7 @@ <?php /** - * InvalidConfigException class file. - * * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ @@ -15,14 +13,14 @@ namespace yii\base; * @author Qiang Xue <qiang.xue@gmail.com> * @since 2.0 */ -class InvalidConfigException extends \Exception +class InvalidConfigException extends Exception { /** * @return string the user-friendly name of this exception */ public function getName() { - return \Yii::t('yii', 'Invalid Configuration'); + return \Yii::t('yii|Invalid Configuration'); } } diff --git a/framework/base/InvalidParamException.php b/framework/base/InvalidParamException.php new file mode 100644 index 0000000..a8c96fd --- /dev/null +++ b/framework/base/InvalidParamException.php @@ -0,0 +1,26 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\base; + +/** + * InvalidParamException represents an exception caused by invalid parameters passed to a method. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class InvalidParamException extends Exception +{ + /** + * @return string the user-friendly name of this exception + */ + public function getName() + { + return \Yii::t('yii|Invalid Parameter'); + } +} + diff --git a/framework/base/InvalidRequestException.php b/framework/base/InvalidRequestException.php index fd468a1..6663e29 100644 --- a/framework/base/InvalidRequestException.php +++ b/framework/base/InvalidRequestException.php @@ -1,9 +1,7 @@ <?php /** - * InvalidRequestException class file. - * * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ @@ -15,19 +13,14 @@ namespace yii\base; * @author Qiang Xue <qiang.xue@gmail.com> * @since 2.0 */ -class InvalidRequestException extends \Exception +class InvalidRequestException extends UserException { /** - * @var boolean whether this exception is caused by end user's mistake (e.g. wrong URL) - */ - public $causedByUser = true; - - /** * @return string the user-friendly name of this exception */ public function getName() { - return \Yii::t('yii', 'Invalid Request'); + return \Yii::t('yii|Invalid Request'); } } diff --git a/framework/base/InvalidRouteException.php b/framework/base/InvalidRouteException.php index e20b2b7..6d2256e 100644 --- a/framework/base/InvalidRouteException.php +++ b/framework/base/InvalidRouteException.php @@ -1,9 +1,7 @@ <?php /** - * InvalidRouteException class file. - * * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ @@ -15,19 +13,14 @@ namespace yii\base; * @author Qiang Xue <qiang.xue@gmail.com> * @since 2.0 */ -class InvalidRouteException extends \Exception +class InvalidRouteException extends UserException { /** - * @var boolean whether this exception is caused by end user's mistake (e.g. wrong URL) - */ - public $causedByUser = true; - - /** * @return string the user-friendly name of this exception */ public function getName() { - return \Yii::t('yii', 'Invalid Route'); + return \Yii::t('yii|Invalid Route'); } } diff --git a/framework/base/Model.php b/framework/base/Model.php index 30cbcfa..13e567d 100644 --- a/framework/base/Model.php +++ b/framework/base/Model.php @@ -1,15 +1,13 @@ <?php /** - * Model class file. - * * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ namespace yii\base; -use yii\util\StringHelper; +use yii\helpers\StringHelper; use yii\validators\Validator; use yii\validators\RequiredValidator; @@ -260,7 +258,7 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess */ public function beforeValidate() { - $event = new ModelEvent($this); + $event = new ModelEvent; $this->trigger(self::EVENT_BEFORE_VALIDATE, $event); return $event->isValid; } @@ -331,7 +329,7 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess foreach ($this->rules() as $rule) { if ($rule instanceof Validator) { $validators->add($rule); - } elseif (isset($rule[0], $rule[1])) { // attributes, validator type + } elseif (is_array($rule) && isset($rule[0], $rule[1])) { // attributes, validator type $validator = Validator::createValidator($rule[1], $this, $rule[0], array_slice($rule, 2)); $validators->add($validator); } else { @@ -422,12 +420,31 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess } /** + * Returns the first error of every attribute in the model. + * @return array the first errors. An empty array will be returned if there is no error. + */ + public function getFirstErrors() + { + if (empty($this->_errors)) { + return array(); + } else { + $errors = array(); + foreach ($this->_errors as $attributeErrors) { + if (isset($attributeErrors[0])) { + $errors[] = $attributeErrors[0]; + } + } + } + return $errors; + } + + /** * Returns the first error of the specified attribute. * @param string $attribute attribute name. * @return string the error message. Null is returned if no error. * @see getErrors */ - public function getError($attribute) + public function getFirstError($attribute) { return isset($this->_errors[$attribute]) ? reset($this->_errors[$attribute]) : null; } @@ -443,25 +460,6 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess } /** - * Adds a list of errors. - * @param array $errors a list of errors. The array keys must be attribute names. - * The array values should be error messages. If an attribute has multiple errors, - * these errors must be given in terms of an array. - */ - public function addErrors($errors) - { - foreach ($errors as $attribute => $error) { - if (is_array($error)) { - foreach ($error as $e) { - $this->_errors[$attribute][] = $e; - } - } else { - $this->_errors[$attribute][] = $error; - } - } - } - - /** * Removes errors for all attributes or a single attribute. * @param string $attribute attribute name. Use null to remove errors for all attribute. */ @@ -543,7 +541,7 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess public function onUnsafeAttribute($name, $value) { if (YII_DEBUG) { - \Yii::warning("Failed to set unsafe attribute '$name' in '" . get_class($this) . "'."); + \Yii::info("Failed to set unsafe attribute '$name' in '" . get_class($this) . "'.", __CLASS__); } } @@ -658,13 +656,13 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess } /** - * Unsets the element at the specified offset. + * Sets the element value at the specified offset to null. * This method is required by the SPL interface `ArrayAccess`. * It is implicitly called when you use something like `unset($model[$offset])`. * @param mixed $offset the offset to unset element */ public function offsetUnset($offset) { - unset($this->$offset); + $this->$offset = null; } } diff --git a/framework/base/ModelEvent.php b/framework/base/ModelEvent.php index e7b6a2e..57e41f9 100644 --- a/framework/base/ModelEvent.php +++ b/framework/base/ModelEvent.php @@ -1,9 +1,7 @@ <?php /** - * ModelEvent class file. - * * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ diff --git a/framework/base/Module.php b/framework/base/Module.php index dcb468c..6b82157 100644 --- a/framework/base/Module.php +++ b/framework/base/Module.php @@ -1,624 +1,623 @@ -<?php -/** - * Module class file. - * - * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC - * @license http://www.yiiframework.com/license/ - */ - -namespace yii\base; - -use Yii; -use yii\util\StringHelper; -use yii\util\FileHelper; - -/** - * Module is the base class for module and application classes. - * - * A module represents a sub-application which contains MVC elements by itself, such as - * models, views, controllers, etc. - * - * A module may consist of [[modules|sub-modules]]. - * - * [[components|Components]] may be registered with the module so that they are globally - * accessible within the module. - * - * @property string $uniqueId An ID that uniquely identifies this module among all modules within - * the current application. - * @property string $basePath The root directory of the module. Defaults to the directory containing the module class. - * @property string $controllerPath The directory containing the controller classes. Defaults to "[[basePath]]/controllers". - * @property string $viewPath The directory containing the view files within this module. Defaults to "[[basePath]]/views". - * @property string $layoutPath The directory containing the layout view files within this module. Defaults to "[[viewPath]]/layouts". - * @property array $modules The configuration of the currently installed modules (module ID => configuration). - * @property array $components The components (indexed by their IDs) registered within this module. - * @property array $import List of aliases to be imported. This property is write-only. - * @property array $aliases List of aliases to be defined. This property is write-only. - * - * @author Qiang Xue <qiang.xue@gmail.com> - * @since 2.0 - */ -abstract class Module extends Component -{ - /** - * @var array custom module parameters (name => value). - */ - public $params = array(); - /** - * @var array the IDs of the components that should be preloaded when this module is created. - */ - public $preload = array(); - /** - * @var string an ID that uniquely identifies this module among other modules which have the same [[module|parent]]. - */ - public $id; - /** - * @var Module the parent module of this module. Null if this module does not have a parent. - */ - public $module; - /** - * @var string|boolean the layout that should be applied for views within this module. This refers to a view name - * relative to [[layoutPath]]. If this is not set, it means the layout value of the [[module|parent module]] - * will be taken. If this is false, layout will be disabled within this module. - */ - public $layout; - /** - * @var array mapping from controller ID to controller configurations. - * Each name-value pair specifies the configuration of a single controller. - * A controller configuration can be either a string or an array. - * If the former, the string should be the class name or path alias of the controller. - * If the latter, the array must contain a 'class' element which specifies - * the controller's class name or path alias, and the rest of the name-value pairs - * in the array are used to initialize the corresponding controller properties. For example, - * - * ~~~ - * array( - * 'account' => '@application/controllers/UserController', - * 'article' => array( - * 'class' => '@application/controllers/PostController', - * 'pageTitle' => 'something new', - * ), - * ) - * ~~~ - */ - public $controllerMap = array(); - /** - * @var string the namespace that controller classes are in. Default is to use global namespace. - */ - public $controllerNamespace; - /** - * @return string the default route of this module. Defaults to 'default'. - * The route may consist of child module ID, controller ID, and/or action ID. - * For example, `help`, `post/create`, `admin/post/create`. - * If action ID is not given, it will take the default value as specified in - * [[Controller::defaultAction]]. - */ - public $defaultRoute = 'default'; - /** - * @var string the root directory of the module. - */ - private $_basePath; - /** - * @var string the root directory that contains view files for this module - */ - private $_viewPath; - /** - * @var string the root directory that contains layout view files for this module. - */ - private $_layoutPath; - /** - * @var string the directory containing controller classes in the module. - */ - private $_controllerPath; - /** - * @var array child modules of this module - */ - private $_modules = array(); - /** - * @var array components registered under this module - */ - private $_components = array(); - - /** - * Constructor. - * @param string $id the ID of this module - * @param Module $parent the parent module (if any) - * @param array $config name-value pairs that will be used to initialize the object properties - */ - public function __construct($id, $parent = null, $config = array()) - { - $this->id = $id; - $this->module = $parent; - parent::__construct($config); - } - - /** - * Getter magic method. - * This method is overridden to support accessing components - * like reading module properties. - * @param string $name component or property name - * @return mixed the named property value - */ - public function __get($name) - { - if ($this->hasComponent($name)) { - return $this->getComponent($name); - } else { - return parent::__get($name); - } - } - - /** - * Checks if a property value is null. - * This method overrides the parent implementation by checking - * if the named component is loaded. - * @param string $name the property name or the event name - * @return boolean whether the property value is null - */ - public function __isset($name) - { - if ($this->hasComponent($name)) { - return $this->getComponent($name) !== null; - } else { - return parent::__isset($name); - } - } - - /** - * Initializes the module. - * This method is called after the module is created and initialized with property values - * given in configuration. The default implement will create a path alias using the module [[id]] - * and then call [[preloadComponents()]] to load components that are declared in [[preload]]. - */ - public function init() - { - Yii::setAlias('@' . $this->id, $this->getBasePath()); - $this->preloadComponents(); - } - - /** - * Returns an ID that uniquely identifies this module among all modules within the current application. - * Note that if the module is an application, an empty string will be returned. - * @return string the unique ID of the module. - */ - public function getUniqueId() - { - if ($this instanceof Application) { - return ''; - } elseif ($this->module) { - return $this->module->getUniqueId() . '/' . $this->id; - } else { - return $this->id; - } - } - - /** - * Returns the root directory of the module. - * It defaults to the directory containing the module class file. - * @return string the root directory of the module. - */ - public function getBasePath() - { - if ($this->_basePath === null) { - $class = new \ReflectionClass($this); - $this->_basePath = dirname($class->getFileName()); - } - return $this->_basePath; - } - - /** - * Sets the root directory of the module. - * This method can only be invoked at the beginning of the constructor. - * @param string $path the root directory of the module. This can be either a directory name or a path alias. - * @throws Exception if the directory does not exist. - */ - public function setBasePath($path) - { - $this->_basePath = FileHelper::ensureDirectory($path); - } - - /** - * Returns the directory that contains the controller classes. - * Defaults to "[[basePath]]/controllers". - * @return string the directory that contains the controller classes. - */ - public function getControllerPath() - { - if ($this->_controllerPath !== null) { - return $this->_controllerPath; - } else { - return $this->_controllerPath = $this->getBasePath() . DIRECTORY_SEPARATOR . 'controllers'; - } - } - - /** - * Sets the directory that contains the controller classes. - * @param string $path the directory that contains the controller classes. - * This can be either a directory name or a path alias. - * @throws Exception if the directory is invalid - */ - public function setControllerPath($path) - { - $this->_controllerPath = FileHelper::ensureDirectory($path); - } - - /** - * Returns the directory that contains the view files for this module. - * @return string the root directory of view files. Defaults to "[[basePath]]/view". - */ - public function getViewPath() - { - if ($this->_viewPath !== null) { - return $this->_viewPath; - } else { - return $this->_viewPath = $this->getBasePath() . DIRECTORY_SEPARATOR . 'views'; - } - } - - /** - * Sets the directory that contains the view files. - * @param string $path the root directory of view files. - * @throws Exception if the directory is invalid - */ - public function setViewPath($path) - { - $this->_viewPath = FileHelper::ensureDirectory($path); - } - - /** - * Returns the directory that contains layout view files for this module. - * @return string the root directory of layout files. Defaults to "[[viewPath]]/layouts". - */ - public function getLayoutPath() - { - if ($this->_layoutPath !== null) { - return $this->_layoutPath; - } else { - return $this->_layoutPath = $this->getViewPath() . DIRECTORY_SEPARATOR . 'layouts'; - } - } - - /** - * Sets the directory that contains the layout files. - * @param string $path the root directory of layout files. - * @throws Exception if the directory is invalid - */ - public function setLayoutPath($path) - { - $this->_layoutPath = FileHelper::ensureDirectory($path); - } - - /** - * Imports the specified path aliases. - * This method is provided so that you can import a set of path aliases when configuring a module. - * The path aliases will be imported by calling [[Yii::import()]]. - * @param array $aliases list of path aliases to be imported - */ - public function setImport($aliases) - { - foreach ($aliases as $alias) { - Yii::import($alias); - } - } - - /** - * Defines path aliases. - * This method calls [[Yii::setAlias()]] to register the path aliases. - * This method is provided so that you can define path aliases when configuring a module. - * @param array $aliases list of path aliases to be defined. The array keys are alias names - * (must start with '@') and the array values are the corresponding paths or aliases. - * For example, - * - * ~~~ - * array( - * '@models' => '@application/models', // an existing alias - * '@backend' => __DIR__ . '/../backend', // a directory - * ) - * ~~~ - */ - public function setAliases($aliases) - { - foreach ($aliases as $name => $alias) { - Yii::setAlias($name, $alias); - } - } - - /** - * Checks whether the named module exists. - * @param string $id module ID - * @return boolean whether the named module exists. Both loaded and unloaded modules - * are considered. - */ - public function hasModule($id) - { - return isset($this->_modules[$id]); - } - - /** - * Retrieves the named module. - * @param string $id module ID (case-sensitive) - * @param boolean $load whether to load the module if it is not yet loaded. - * @return Module|null the module instance, null if the module - * does not exist. - * @see hasModule() - */ - public function getModule($id, $load = true) - { - if (isset($this->_modules[$id])) { - if ($this->_modules[$id] instanceof Module) { - return $this->_modules[$id]; - } elseif ($load) { - Yii::trace("Loading module: $id", __CLASS__); - return $this->_modules[$id] = Yii::createObject($this->_modules[$id], $id, $this); - } - } - return null; - } - - /** - * Adds a sub-module to this module. - * @param string $id module ID - * @param Module|array|null $module the sub-module to be added to this module. This can - * be one of the followings: - * - * - a [[Module]] object - * - a configuration array: when [[getModule()]] is called initially, the array - * will be used to instantiate the sub-module - * - null: the named sub-module will be removed from this module - */ - public function setModule($id, $module) - { - if ($module === null) { - unset($this->_modules[$id]); - } else { - $this->_modules[$id] = $module; - } - } - - /** - * Returns the sub-modules in this module. - * @param boolean $loadedOnly whether to return the loaded sub-modules only. If this is set false, - * then all sub-modules registered in this module will be returned, whether they are loaded or not. - * Loaded modules will be returned as objects, while unloaded modules as configuration arrays. - * @return array the modules (indexed by their IDs) - */ - public function getModules($loadedOnly = false) - { - if ($loadedOnly) { - $modules = array(); - foreach ($this->_modules as $module) { - if ($module instanceof Module) { - $modules[] = $module; - } - } - return $modules; - } else { - return $this->_modules; - } - } - - /** - * Registers sub-modules in the current module. - * - * Each sub-module should be specified as a name-value pair, where - * name refers to the ID of the module and value the module or a configuration - * array that can be used to create the module. In the latter case, [[Yii::createObject()]] - * will be used to create the module. - * - * If a new sub-module has the same ID as an existing one, the existing one will be overwritten silently. - * - * The following is an example for registering two sub-modules: - * - * ~~~ - * array( - * 'comment' => array( - * 'class' => 'app\modules\CommentModule', - * 'connectionID' => 'db', - * ), - * 'booking' => array( - * 'class' => 'app\modules\BookingModule', - * ), - * ) - * ~~~ - * - * @param array $modules modules (id => module configuration or instances) - */ - public function setModules($modules) - { - foreach ($modules as $id => $module) { - $this->_modules[$id] = $module; - } - } - - /** - * Checks whether the named component exists. - * @param string $id component ID - * @return boolean whether the named component exists. Both loaded and unloaded components - * are considered. - */ - public function hasComponent($id) - { - return isset($this->_components[$id]); - } - - /** - * Retrieves the named component. - * @param string $id component ID (case-sensitive) - * @param boolean $load whether to load the component if it is not yet loaded. - * @return Component|null the component instance, null if the component does not exist. - * @see hasComponent() - */ - public function getComponent($id, $load = true) - { - if (isset($this->_components[$id])) { - if ($this->_components[$id] instanceof Component) { - return $this->_components[$id]; - } elseif ($load) { - Yii::trace("Loading component: $id", __CLASS__); - return $this->_components[$id] = Yii::createObject($this->_components[$id]); - } - } - return null; - } - - /** - * Registers a component with this module. - * @param string $id component ID - * @param Component|array|null $component the component to be registered with the module. This can - * be one of the followings: - * - * - a [[Component]] object - * - a configuration array: when [[getComponent()]] is called initially for this component, the array - * will be used to instantiate the component via [[Yii::createObject()]]. - * - null: the named component will be removed from the module - */ - public function setComponent($id, $component) - { - if ($component === null) { - unset($this->_components[$id]); - } else { - $this->_components[$id] = $component; - } - } - - /** - * Returns the registered components. - * @param boolean $loadedOnly whether to return the loaded components only. If this is set false, - * then all components specified in the configuration will be returned, whether they are loaded or not. - * Loaded components will be returned as objects, while unloaded components as configuration arrays. - * @return array the components (indexed by their IDs) - */ - public function getComponents($loadedOnly = false) - { - if ($loadedOnly) { - $components = array(); - foreach ($this->_components as $component) { - if ($component instanceof Component) { - $components[] = $component; - } - } - return $components; - } else { - return $this->_components; - } - } - - /** - * Registers a set of components in this module. - * - * Each component should be specified as a name-value pair, where - * name refers to the ID of the component and value the component or a configuration - * array that can be used to create the component. In the latter case, [[Yii::createObject()]] - * will be used to create the component. - * - * If a new component has the same ID as an existing one, the existing one will be overwritten silently. - * - * The following is an example for setting two components: - * - * ~~~ - * array( - * 'db' => array( - * 'class' => 'yii\db\Connection', - * 'dsn' => 'sqlite:path/to/file.db', - * ), - * 'cache' => array( - * 'class' => 'yii\caching\DbCache', - * 'connectionID' => 'db', - * ), - * ) - * ~~~ - * - * @param array $components components (id => component configuration or instance) - */ - public function setComponents($components) - { - foreach ($components as $id => $component) { - $this->_components[$id] = $component; - } - } - - /** - * Loads components that are declared in [[preload]]. - */ - public function preloadComponents() - { - foreach ($this->preload as $id) { - $this->getComponent($id); - } - } - - /** - * Runs a controller action specified by a route. - * This method parses the specified route and creates the corresponding child module(s), controller and action - * instances. It then calls [[Controller::runAction()]] to run the action with the given parameters. - * If the route is empty, the method will use [[defaultRoute]]. - * @param string $route the route that specifies the action. - * @param array $params the parameters to be passed to the action - * @return integer the status code returned by the action execution. 0 means normal, and other values mean abnormal. - * @throws InvalidRouteException if the requested route cannot be resolved into an action successfully - */ - public function runAction($route, $params = array()) - { - $result = $this->createController($route); - if (is_array($result)) { - /** @var $controller Controller */ - list($controller, $actionID) = $result; - $oldController = Yii::$application->controller; - Yii::$application->controller = $controller; - $status = $controller->runAction($actionID, $params); - Yii::$application->controller = $oldController; - return $status; - } else { - throw new InvalidRouteException('Unable to resolve the request: ' . trim($this->getUniqueId() . '/' . $route, '/')); - } - } - - /** - * Creates a controller instance based on the controller ID. - * - * The controller is created within this module. The method first attempts to - * create the controller based on the [[controllerMap]] of the module. If not available, - * it will look for the controller class under the [[controllerPath]] and create an - * instance of it. - * - * @param string $route the route consisting of module, controller and action IDs. - * @return array|boolean if the controller is created successfully, it will be returned together - * with the remainder of the route which represents the action ID. Otherwise false will be returned. - */ - public function createController($route) - { - if ($route === '') { - $route = $this->defaultRoute; - } - if (($pos = strpos($route, '/')) !== false) { - $id = substr($route, 0, $pos); - $route = substr($route, $pos + 1); - } else { - $id = $route; - $route = ''; - } - - $module = $this->getModule($id); - if ($module !== null) { - return $module->createController($route); - } - - if (isset($this->controllerMap[$id])) { - $controller = Yii::createObject($this->controllerMap[$id], $id, $this); - } elseif (preg_match('/^[a-z0-9\\-_]+$/', $id)) { - $className = StringHelper::id2camel($id) . 'Controller'; - $classFile = $this->controllerPath . DIRECTORY_SEPARATOR . $className . '.php'; - if (is_file($classFile)) { - $className = $this->controllerNamespace . '\\' . $className; - if (!class_exists($className, false)) { - require($classFile); - } - if (class_exists($className, false) && is_subclass_of($className, '\yii\base\Controller')) { - $controller = new $className($id, $this); - } - } - } - - return isset($controller) ? array($controller, $route) : false; - } -} +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\base; + +use Yii; +use yii\helpers\StringHelper; +use yii\helpers\FileHelper; + +/** + * Module is the base class for module and application classes. + * + * A module represents a sub-application which contains MVC elements by itself, such as + * models, views, controllers, etc. + * + * A module may consist of [[modules|sub-modules]]. + * + * [[components|Components]] may be registered with the module so that they are globally + * accessible within the module. + * + * @property string $uniqueId An ID that uniquely identifies this module among all modules within + * the current application. + * @property string $basePath The root directory of the module. Defaults to the directory containing the module class. + * @property string $controllerPath The directory containing the controller classes. Defaults to "[[basePath]]/controllers". + * @property string $viewPath The directory containing the view files within this module. Defaults to "[[basePath]]/views". + * @property string $layoutPath The directory containing the layout view files within this module. Defaults to "[[viewPath]]/layouts". + * @property array $modules The configuration of the currently installed modules (module ID => configuration). + * @property array $components The components (indexed by their IDs) registered within this module. + * @property array $import List of aliases to be imported. This property is write-only. + * @property array $aliases List of aliases to be defined. This property is write-only. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +abstract class Module extends Component +{ + /** + * @var array custom module parameters (name => value). + */ + public $params = array(); + /** + * @var array the IDs of the components that should be preloaded when this module is created. + */ + public $preload = array(); + /** + * @var string an ID that uniquely identifies this module among other modules which have the same [[module|parent]]. + */ + public $id; + /** + * @var Module the parent module of this module. Null if this module does not have a parent. + */ + public $module; + /** + * @var string|boolean the layout that should be applied for views within this module. This refers to a view name + * relative to [[layoutPath]]. If this is not set, it means the layout value of the [[module|parent module]] + * will be taken. If this is false, layout will be disabled within this module. + */ + public $layout; + /** + * @var array mapping from controller ID to controller configurations. + * Each name-value pair specifies the configuration of a single controller. + * A controller configuration can be either a string or an array. + * If the former, the string should be the class name or path alias of the controller. + * If the latter, the array must contain a 'class' element which specifies + * the controller's class name or path alias, and the rest of the name-value pairs + * in the array are used to initialize the corresponding controller properties. For example, + * + * ~~~ + * array( + * 'account' => '@app/controllers/UserController', + * 'article' => array( + * 'class' => '@app/controllers/PostController', + * 'pageTitle' => 'something new', + * ), + * ) + * ~~~ + */ + public $controllerMap = array(); + /** + * @var string the namespace that controller classes are in. Default is to use global namespace. + */ + public $controllerNamespace; + /** + * @return string the default route of this module. Defaults to 'default'. + * The route may consist of child module ID, controller ID, and/or action ID. + * For example, `help`, `post/create`, `admin/post/create`. + * If action ID is not given, it will take the default value as specified in + * [[Controller::defaultAction]]. + */ + public $defaultRoute = 'default'; + /** + * @var string the root directory of the module. + */ + private $_basePath; + /** + * @var string the root directory that contains view files for this module + */ + private $_viewPath; + /** + * @var string the root directory that contains layout view files for this module. + */ + private $_layoutPath; + /** + * @var string the directory containing controller classes in the module. + */ + private $_controllerPath; + /** + * @var array child modules of this module + */ + private $_modules = array(); + /** + * @var array components registered under this module + */ + private $_components = array(); + + /** + * Constructor. + * @param string $id the ID of this module + * @param Module $parent the parent module (if any) + * @param array $config name-value pairs that will be used to initialize the object properties + */ + public function __construct($id, $parent = null, $config = array()) + { + $this->id = $id; + $this->module = $parent; + parent::__construct($config); + } + + /** + * Getter magic method. + * This method is overridden to support accessing components + * like reading module properties. + * @param string $name component or property name + * @return mixed the named property value + */ + public function __get($name) + { + if ($this->hasComponent($name)) { + return $this->getComponent($name); + } else { + return parent::__get($name); + } + } + + /** + * Checks if a property value is null. + * This method overrides the parent implementation by checking + * if the named component is loaded. + * @param string $name the property name or the event name + * @return boolean whether the property value is null + */ + public function __isset($name) + { + if ($this->hasComponent($name)) { + return $this->getComponent($name) !== null; + } else { + return parent::__isset($name); + } + } + + /** + * Initializes the module. + * This method is called after the module is created and initialized with property values + * given in configuration. The default implement will create a path alias using the module [[id]] + * and then call [[preloadComponents()]] to load components that are declared in [[preload]]. + */ + public function init() + { + Yii::setAlias('@' . $this->id, $this->getBasePath()); + $this->preloadComponents(); + } + + /** + * Returns an ID that uniquely identifies this module among all modules within the current application. + * Note that if the module is an application, an empty string will be returned. + * @return string the unique ID of the module. + */ + public function getUniqueId() + { + if ($this instanceof Application) { + return ''; + } elseif ($this->module) { + return $this->module->getUniqueId() . '/' . $this->id; + } else { + return $this->id; + } + } + + /** + * Returns the root directory of the module. + * It defaults to the directory containing the module class file. + * @return string the root directory of the module. + */ + public function getBasePath() + { + if ($this->_basePath === null) { + $class = new \ReflectionClass($this); + $this->_basePath = dirname($class->getFileName()); + } + return $this->_basePath; + } + + /** + * Sets the root directory of the module. + * This method can only be invoked at the beginning of the constructor. + * @param string $path the root directory of the module. This can be either a directory name or a path alias. + * @throws Exception if the directory does not exist. + */ + public function setBasePath($path) + { + $this->_basePath = FileHelper::ensureDirectory($path); + } + + /** + * Returns the directory that contains the controller classes. + * Defaults to "[[basePath]]/controllers". + * @return string the directory that contains the controller classes. + */ + public function getControllerPath() + { + if ($this->_controllerPath !== null) { + return $this->_controllerPath; + } else { + return $this->_controllerPath = $this->getBasePath() . DIRECTORY_SEPARATOR . 'controllers'; + } + } + + /** + * Sets the directory that contains the controller classes. + * @param string $path the directory that contains the controller classes. + * This can be either a directory name or a path alias. + * @throws Exception if the directory is invalid + */ + public function setControllerPath($path) + { + $this->_controllerPath = FileHelper::ensureDirectory($path); + } + + /** + * Returns the directory that contains the view files for this module. + * @return string the root directory of view files. Defaults to "[[basePath]]/view". + */ + public function getViewPath() + { + if ($this->_viewPath !== null) { + return $this->_viewPath; + } else { + return $this->_viewPath = $this->getBasePath() . DIRECTORY_SEPARATOR . 'views'; + } + } + + /** + * Sets the directory that contains the view files. + * @param string $path the root directory of view files. + * @throws Exception if the directory is invalid + */ + public function setViewPath($path) + { + $this->_viewPath = FileHelper::ensureDirectory($path); + } + + /** + * Returns the directory that contains layout view files for this module. + * @return string the root directory of layout files. Defaults to "[[viewPath]]/layouts". + */ + public function getLayoutPath() + { + if ($this->_layoutPath !== null) { + return $this->_layoutPath; + } else { + return $this->_layoutPath = $this->getViewPath() . DIRECTORY_SEPARATOR . 'layouts'; + } + } + + /** + * Sets the directory that contains the layout files. + * @param string $path the root directory of layout files. + * @throws Exception if the directory is invalid + */ + public function setLayoutPath($path) + { + $this->_layoutPath = FileHelper::ensureDirectory($path); + } + + /** + * Imports the specified path aliases. + * This method is provided so that you can import a set of path aliases when configuring a module. + * The path aliases will be imported by calling [[Yii::import()]]. + * @param array $aliases list of path aliases to be imported + */ + public function setImport($aliases) + { + foreach ($aliases as $alias) { + Yii::import($alias); + } + } + + /** + * Defines path aliases. + * This method calls [[Yii::setAlias()]] to register the path aliases. + * This method is provided so that you can define path aliases when configuring a module. + * @param array $aliases list of path aliases to be defined. The array keys are alias names + * (must start with '@') and the array values are the corresponding paths or aliases. + * For example, + * + * ~~~ + * array( + * '@models' => '@app/models', // an existing alias + * '@backend' => __DIR__ . '/../backend', // a directory + * ) + * ~~~ + */ + public function setAliases($aliases) + { + foreach ($aliases as $name => $alias) { + Yii::setAlias($name, $alias); + } + } + + /** + * Checks whether the named module exists. + * @param string $id module ID + * @return boolean whether the named module exists. Both loaded and unloaded modules + * are considered. + */ + public function hasModule($id) + { + return isset($this->_modules[$id]); + } + + /** + * Retrieves the named module. + * @param string $id module ID (case-sensitive) + * @param boolean $load whether to load the module if it is not yet loaded. + * @return Module|null the module instance, null if the module + * does not exist. + * @see hasModule() + */ + public function getModule($id, $load = true) + { + if (isset($this->_modules[$id])) { + if ($this->_modules[$id] instanceof Module) { + return $this->_modules[$id]; + } elseif ($load) { + Yii::trace("Loading module: $id", __CLASS__); + return $this->_modules[$id] = Yii::createObject($this->_modules[$id], $id, $this); + } + } + return null; + } + + /** + * Adds a sub-module to this module. + * @param string $id module ID + * @param Module|array|null $module the sub-module to be added to this module. This can + * be one of the followings: + * + * - a [[Module]] object + * - a configuration array: when [[getModule()]] is called initially, the array + * will be used to instantiate the sub-module + * - null: the named sub-module will be removed from this module + */ + public function setModule($id, $module) + { + if ($module === null) { + unset($this->_modules[$id]); + } else { + $this->_modules[$id] = $module; + } + } + + /** + * Returns the sub-modules in this module. + * @param boolean $loadedOnly whether to return the loaded sub-modules only. If this is set false, + * then all sub-modules registered in this module will be returned, whether they are loaded or not. + * Loaded modules will be returned as objects, while unloaded modules as configuration arrays. + * @return array the modules (indexed by their IDs) + */ + public function getModules($loadedOnly = false) + { + if ($loadedOnly) { + $modules = array(); + foreach ($this->_modules as $module) { + if ($module instanceof Module) { + $modules[] = $module; + } + } + return $modules; + } else { + return $this->_modules; + } + } + + /** + * Registers sub-modules in the current module. + * + * Each sub-module should be specified as a name-value pair, where + * name refers to the ID of the module and value the module or a configuration + * array that can be used to create the module. In the latter case, [[Yii::createObject()]] + * will be used to create the module. + * + * If a new sub-module has the same ID as an existing one, the existing one will be overwritten silently. + * + * The following is an example for registering two sub-modules: + * + * ~~~ + * array( + * 'comment' => array( + * 'class' => 'app\modules\CommentModule', + * 'db' => 'db', + * ), + * 'booking' => array( + * 'class' => 'app\modules\BookingModule', + * ), + * ) + * ~~~ + * + * @param array $modules modules (id => module configuration or instances) + */ + public function setModules($modules) + { + foreach ($modules as $id => $module) { + $this->_modules[$id] = $module; + } + } + + /** + * Checks whether the named component exists. + * @param string $id component ID + * @return boolean whether the named component exists. Both loaded and unloaded components + * are considered. + */ + public function hasComponent($id) + { + return isset($this->_components[$id]); + } + + /** + * Retrieves the named component. + * @param string $id component ID (case-sensitive) + * @param boolean $load whether to load the component if it is not yet loaded. + * @return Component|null the component instance, null if the component does not exist. + * @see hasComponent() + */ + public function getComponent($id, $load = true) + { + if (isset($this->_components[$id])) { + if ($this->_components[$id] instanceof Component) { + return $this->_components[$id]; + } elseif ($load) { + Yii::trace("Loading component: $id", __CLASS__); + return $this->_components[$id] = Yii::createObject($this->_components[$id]); + } + } + return null; + } + + /** + * Registers a component with this module. + * @param string $id component ID + * @param Component|array|null $component the component to be registered with the module. This can + * be one of the followings: + * + * - a [[Component]] object + * - a configuration array: when [[getComponent()]] is called initially for this component, the array + * will be used to instantiate the component via [[Yii::createObject()]]. + * - null: the named component will be removed from the module + */ + public function setComponent($id, $component) + { + if ($component === null) { + unset($this->_components[$id]); + } else { + $this->_components[$id] = $component; + } + } + + /** + * Returns the registered components. + * @param boolean $loadedOnly whether to return the loaded components only. If this is set false, + * then all components specified in the configuration will be returned, whether they are loaded or not. + * Loaded components will be returned as objects, while unloaded components as configuration arrays. + * @return array the components (indexed by their IDs) + */ + public function getComponents($loadedOnly = false) + { + if ($loadedOnly) { + $components = array(); + foreach ($this->_components as $component) { + if ($component instanceof Component) { + $components[] = $component; + } + } + return $components; + } else { + return $this->_components; + } + } + + /** + * Registers a set of components in this module. + * + * Each component should be specified as a name-value pair, where + * name refers to the ID of the component and value the component or a configuration + * array that can be used to create the component. In the latter case, [[Yii::createObject()]] + * will be used to create the component. + * + * If a new component has the same ID as an existing one, the existing one will be overwritten silently. + * + * The following is an example for setting two components: + * + * ~~~ + * array( + * 'db' => array( + * 'class' => 'yii\db\Connection', + * 'dsn' => 'sqlite:path/to/file.db', + * ), + * 'cache' => array( + * 'class' => 'yii\caching\DbCache', + * 'db' => 'db', + * ), + * ) + * ~~~ + * + * @param array $components components (id => component configuration or instance) + */ + public function setComponents($components) + { + foreach ($components as $id => $component) { + $this->_components[$id] = $component; + } + } + + /** + * Loads components that are declared in [[preload]]. + */ + public function preloadComponents() + { + foreach ($this->preload as $id) { + $this->getComponent($id); + } + } + + /** + * Runs a controller action specified by a route. + * This method parses the specified route and creates the corresponding child module(s), controller and action + * instances. It then calls [[Controller::runAction()]] to run the action with the given parameters. + * If the route is empty, the method will use [[defaultRoute]]. + * @param string $route the route that specifies the action. + * @param array $params the parameters to be passed to the action + * @return integer the status code returned by the action execution. 0 means normal, and other values mean abnormal. + * @throws InvalidRouteException if the requested route cannot be resolved into an action successfully + */ + public function runAction($route, $params = array()) + { + $result = $this->createController($route); + if (is_array($result)) { + /** @var $controller Controller */ + list($controller, $actionID) = $result; + $oldController = Yii::$app->controller; + Yii::$app->controller = $controller; + $status = $controller->runAction($actionID, $params); + Yii::$app->controller = $oldController; + return $status; + } else { + throw new InvalidRouteException('Unable to resolve the request "' . trim($this->getUniqueId() . '/' . $route, '/') . '".'); + } + } + + /** + * Creates a controller instance based on the controller ID. + * + * The controller is created within this module. The method first attempts to + * create the controller based on the [[controllerMap]] of the module. If not available, + * it will look for the controller class under the [[controllerPath]] and create an + * instance of it. + * + * @param string $route the route consisting of module, controller and action IDs. + * @return array|boolean if the controller is created successfully, it will be returned together + * with the remainder of the route which represents the action ID. Otherwise false will be returned. + */ + public function createController($route) + { + if ($route === '') { + $route = $this->defaultRoute; + } + if (($pos = strpos($route, '/')) !== false) { + $id = substr($route, 0, $pos); + $route = substr($route, $pos + 1); + } else { + $id = $route; + $route = ''; + } + + $module = $this->getModule($id); + if ($module !== null) { + return $module->createController($route); + } + + if (isset($this->controllerMap[$id])) { + $controller = Yii::createObject($this->controllerMap[$id], $id, $this); + } elseif (preg_match('/^[a-z0-9\\-_]+$/', $id)) { + $className = StringHelper::id2camel($id) . 'Controller'; + + $classFile = $this->controllerPath . DIRECTORY_SEPARATOR . $className . '.php'; + if (is_file($classFile)) { + $className = $this->controllerNamespace . '\\' . $className; + if (!class_exists($className, false)) { + require($classFile); + } + if (class_exists($className, false) && is_subclass_of($className, '\yii\base\Controller')) { + $controller = new $className($id, $this); + } + } + } + + return isset($controller) ? array($controller, $route) : false; + } +} diff --git a/framework/base/NotSupportedException.php b/framework/base/NotSupportedException.php index 56e7e36..2f08891 100644 --- a/framework/base/NotSupportedException.php +++ b/framework/base/NotSupportedException.php @@ -1,9 +1,7 @@ <?php /** - * NotSupportedException class file. - * * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ @@ -15,14 +13,14 @@ namespace yii\base; * @author Qiang Xue <qiang.xue@gmail.com> * @since 2.0 */ -class NotSupportedException extends \Exception +class NotSupportedException extends Exception { /** * @return string the user-friendly name of this exception */ public function getName() { - return \Yii::t('yii', 'Not Supported'); + return \Yii::t('yii|Not Supported'); } } diff --git a/framework/base/Object.php b/framework/base/Object.php index a3425dc..3bd8378 100644 --- a/framework/base/Object.php +++ b/framework/base/Object.php @@ -1,9 +1,7 @@ <?php /** - * Object class file. - * * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ @@ -67,7 +65,7 @@ class Object if (method_exists($this, $getter)) { return $this->$getter(); } else { - throw new UnknownPropertyException('Getting unknown property: ' . get_class($this) . '.' . $name); + throw new UnknownPropertyException('Getting unknown property: ' . get_class($this) . '::' . $name); } } @@ -88,9 +86,9 @@ class Object if (method_exists($this, $setter)) { $this->$setter($value); } elseif (method_exists($this, 'get' . $name)) { - throw new InvalidCallException('Setting read-only property: ' . get_class($this) . '.' . $name); + throw new InvalidCallException('Setting read-only property: ' . get_class($this) . '::' . $name); } else { - throw new UnknownPropertyException('Setting unknown property: ' . get_class($this) . '.' . $name); + throw new UnknownPropertyException('Setting unknown property: ' . get_class($this) . '::' . $name); } } @@ -131,7 +129,7 @@ class Object if (method_exists($this, $setter)) { $this->$setter(null); } elseif (method_exists($this, 'get' . $name)) { - throw new InvalidCallException('Unsetting read-only property: ' . get_class($this) . '.' . $name); + throw new InvalidCallException('Unsetting read-only property: ' . get_class($this) . '::' . $name); } } diff --git a/framework/base/Request.php b/framework/base/Request.php index 0dbc568..45556ab 100644 --- a/framework/base/Request.php +++ b/framework/base/Request.php @@ -1,9 +1,7 @@ <?php /** - * Request class file. - * * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ @@ -13,12 +11,18 @@ namespace yii\base; * @author Qiang Xue <qiang.xue@gmail.com> * @since 2.0 */ -class Request extends Component +abstract class Request extends Component { private $_scriptFile; private $_isConsoleRequest; /** + * Resolves the current request into a route and the associated parameters. + * @return array the first element is the route, and the second is the associated parameters. + */ + abstract public function resolve(); + + /** * Returns a value indicating whether the current request is made via command line * @return boolean the value indicating whether the current request is made via console */ @@ -39,24 +43,35 @@ class Request extends Component /** * Returns entry script file path. * @return string entry script file path (processed w/ realpath()) + * @throws InvalidConfigException if the entry script file path cannot be determined automatically. */ public function getScriptFile() { if ($this->_scriptFile === null) { - $this->_scriptFile = realpath($_SERVER['SCRIPT_FILENAME']); + if (isset($_SERVER['SCRIPT_FILENAME'])) { + $this->setScriptFile($_SERVER['SCRIPT_FILENAME']); + } else { + throw new InvalidConfigException('Unable to determine the entry script file path.'); + } } return $this->_scriptFile; } /** * Sets the entry script file path. - * This can be an absolute or relative file path, or a path alias. - * Note that you normally do not have to set the script file path - * as [[getScriptFile()]] can determine it based on `$_SERVER['SCRIPT_FILENAME']`. - * @param string $value the entry script file + * The entry script file path can normally be determined based on the `SCRIPT_FILENAME` SERVER variable. + * However, for some server configurations, this may not be correct or feasible. + * This setter is provided so that the entry script file path can be manually specified. + * @param string $value the entry script file path. This can be either a file path or a path alias. + * @throws InvalidConfigException if the provided entry script file path is invalid. */ public function setScriptFile($value) { - $this->_scriptFile = realpath(\Yii::getAlias($value)); + $scriptFile = realpath(\Yii::getAlias($value)); + if ($scriptFile !== false && is_file($scriptFile)) { + $this->_scriptFile = $scriptFile; + } else { + throw new InvalidConfigException('Unable to determine the entry script file path.'); + } } } diff --git a/framework/base/Response.php b/framework/base/Response.php index 3ced584..a53fd61 100644 --- a/framework/base/Response.php +++ b/framework/base/Response.php @@ -1,9 +1,7 @@ <?php /** - * Response class file. - * * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ diff --git a/framework/base/SecurityManager.php b/framework/base/SecurityManager.php deleted file mode 100644 index 441b908..0000000 --- a/framework/base/SecurityManager.php +++ /dev/null @@ -1,290 +0,0 @@ -<?php -/** - * SecurityManager class file. - * - * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC - * @license http://www.yiiframework.com/license/ - */ - -namespace yii\base; - -/** - * SecurityManager provides private keys, hashing and encryption functions. - * - * @author Qiang Xue <qiang.xue@gmail.com> - * @since 2.0 - */ -class SecurityManager extends Component -{ - const STATE_VALIDATION_KEY = 'Yii.SecurityManager.validationkey'; - const STATE_ENCRYPTION_KEY = 'Yii.SecurityManager.encryptionkey'; - - /** - * @var string the name of the hashing algorithm to be used by {@link computeHMAC}. - * See {@link http://php.net/manual/en/function.hash-algos.php hash-algos} for the list of possible - * hash algorithms. Note that if you are using PHP 5.1.1 or below, you can only use 'sha1' or 'md5'. - * - * Defaults to 'sha1', meaning using SHA1 hash algorithm. - */ - public $hashAlgorithm = 'sha1'; - /** - * @var mixed the name of the crypt algorithm to be used by {@link encrypt} and {@link decrypt}. - * This will be passed as the first parameter to {@link http://php.net/manual/en/function.mcrypt-module-open.php mcrypt_module_open}. - * - * This property can also be configured as an array. In this case, the array elements will be passed in order - * as parameters to mcrypt_module_open. For example, <code>array('rijndael-256', '', 'ofb', '')</code>. - * - * Defaults to 'des', meaning using DES crypt algorithm. - */ - public $cryptAlgorithm = 'des'; - - private $_validationKey; - private $_encryptionKey; - - /** - * @return string a randomly generated private key - */ - protected function generateRandomKey() - { - return sprintf('%08x%08x%08x%08x', mt_rand(), mt_rand(), mt_rand(), mt_rand()); - } - - /** - * @return string the private key used to generate HMAC. - * If the key is not explicitly set, a random one is generated and returned. - */ - public function getValidationKey() - { - if ($this->_validationKey !== null) { - return $this->_validationKey; - } else { - if (($key = \Yii::$application->getGlobalState(self::STATE_VALIDATION_KEY)) !== null) { - $this->setValidationKey($key); - } else { - $key = $this->generateRandomKey(); - $this->setValidationKey($key); - \Yii::$application->setGlobalState(self::STATE_VALIDATION_KEY, $key); - } - return $this->_validationKey; - } - } - - /** - * @param string $value the key used to generate HMAC - * @throws CException if the key is empty - */ - public function setValidationKey($value) - { - if (!empty($value)) { - $this->_validationKey = $value; - } else { - throw new CException(Yii::t('yii', 'SecurityManager.validationKey cannot be empty.')); - } - } - - /** - * @return string the private key used to encrypt/decrypt data. - * If the key is not explicitly set, a random one is generated and returned. - */ - public function getEncryptionKey() - { - if ($this->_encryptionKey !== null) { - return $this->_encryptionKey; - } else { - if (($key = \Yii::$application->getGlobalState(self::STATE_ENCRYPTION_KEY)) !== null) { - $this->setEncryptionKey($key); - } else { - $key = $this->generateRandomKey(); - $this->setEncryptionKey($key); - \Yii::$application->setGlobalState(self::STATE_ENCRYPTION_KEY, $key); - } - return $this->_encryptionKey; - } - } - - /** - * @param string $value the key used to encrypt/decrypt data. - * @throws CException if the key is empty - */ - public function setEncryptionKey($value) - { - if (!empty($value)) { - $this->_encryptionKey = $value; - } else { - throw new CException(Yii::t('yii', 'SecurityManager.encryptionKey cannot be empty.')); - } - } - - /** - * This method has been deprecated since version 1.1.3. - * Please use {@link hashAlgorithm} instead. - * @return string - */ - public function getValidation() - { - return $this->hashAlgorithm; - } - - /** - * This method has been deprecated since version 1.1.3. - * Please use {@link hashAlgorithm} instead. - * @param string $value - - */ - public function setValidation($value) - { - $this->hashAlgorithm = $value; - } - - /** - * Encrypts data. - * @param string $data data to be encrypted. - * @param string $key the decryption key. This defaults to null, meaning using {@link getEncryptionKey EncryptionKey}. - * @return string the encrypted data - * @throws CException if PHP Mcrypt extension is not loaded - */ - public function encrypt($data, $key = null) - { - $module = $this->openCryptModule(); - $key = $this->substr($key === null ? md5($this->getEncryptionKey()) : $key, 0, mcrypt_enc_get_key_size($module)); - srand(); - $iv = mcrypt_create_iv(mcrypt_enc_get_iv_size($module), MCRYPT_RAND); - mcrypt_generic_init($module, $key, $iv); - $encrypted = $iv . mcrypt_generic($module, $data); - mcrypt_generic_deinit($module); - mcrypt_module_close($module); - return $encrypted; - } - - /** - * Decrypts data - * @param string $data data to be decrypted. - * @param string $key the decryption key. This defaults to null, meaning using {@link getEncryptionKey EncryptionKey}. - * @return string the decrypted data - * @throws CException if PHP Mcrypt extension is not loaded - */ - public function decrypt($data, $key = null) - { - $module = $this->openCryptModule(); - $key = $this->substr($key === null ? md5($this->getEncryptionKey()) : $key, 0, mcrypt_enc_get_key_size($module)); - $ivSize = mcrypt_enc_get_iv_size($module); - $iv = $this->substr($data, 0, $ivSize); - mcrypt_generic_init($module, $key, $iv); - $decrypted = mdecrypt_generic($module, $this->substr($data, $ivSize, $this->strlen($data))); - mcrypt_generic_deinit($module); - mcrypt_module_close($module); - return rtrim($decrypted, "\0"); - } - - /** - * Opens the mcrypt module with the configuration specified in {@link cryptAlgorithm}. - * @return resource the mycrypt module handle. - * @since 1.1.3 - */ - protected function openCryptModule() - { - if (extension_loaded('mcrypt')) { - if (is_array($this->cryptAlgorithm)) { - $module = @call_user_func_array('mcrypt_module_open', $this->cryptAlgorithm); - } else { - $module = @mcrypt_module_open($this->cryptAlgorithm, '', MCRYPT_MODE_CBC, ''); - } - - if ($module === false) { - throw new CException(Yii::t('yii', 'Failed to initialize the mcrypt module.')); - } - - return $module; - } else { - throw new CException(Yii::t('yii', 'SecurityManager requires PHP mcrypt extension to be loaded in order to use data encryption feature.')); - } - } - - /** - * Prefixes data with an HMAC. - * @param string $data data to be hashed. - * @param string $key the private key to be used for generating HMAC. Defaults to null, meaning using {@link validationKey}. - * @return string data prefixed with HMAC - */ - public function hashData($data, $key = null) - { - return $this->computeHMAC($data, $key) . $data; - } - - /** - * Validates if data is tampered. - * @param string $data data to be validated. The data must be previously - * generated using {@link hashData()}. - * @param string $key the private key to be used for generating HMAC. Defaults to null, meaning using {@link validationKey}. - * @return string the real data with HMAC stripped off. False if the data - * is tampered. - */ - public function validateData($data, $key = null) - { - $len = $this->strlen($this->computeHMAC('test')); - if ($this->strlen($data) >= $len) { - $hmac = $this->substr($data, 0, $len); - $data2 = $this->substr($data, $len, $this->strlen($data)); - return $hmac === $this->computeHMAC($data2, $key) ? $data2 : false; - } else { - return false; - } - } - - /** - * Computes the HMAC for the data with {@link getValidationKey ValidationKey}. - * @param string $data data to be generated HMAC - * @param string $key the private key to be used for generating HMAC. Defaults to null, meaning using {@link validationKey}. - * @return string the HMAC for the data - */ - protected function computeHMAC($data, $key = null) - { - if ($key === null) { - $key = $this->getValidationKey(); - } - - if (function_exists('hash_hmac')) { - return hash_hmac($this->hashAlgorithm, $data, $key); - } - - if (!strcasecmp($this->hashAlgorithm, 'sha1')) { - $pack = 'H40'; - $func = 'sha1'; - } else { - $pack = 'H32'; - $func = 'md5'; - } - if ($this->strlen($key) > 64) { - $key = pack($pack, $func($key)); - } - if ($this->strlen($key) < 64) { - $key = str_pad($key, 64, chr(0)); - } - $key = $this->substr($key, 0, 64); - return $func((str_repeat(chr(0x5C), 64) ^ $key) . pack($pack, $func((str_repeat(chr(0x36), 64) ^ $key) . $data))); - } - - /** - * Returns the length of the given string. - * If available uses the multibyte string function mb_strlen. - * @param string $string the string being measured for length - * @return int the length of the string - */ - private function strlen($string) - { - return function_exists('mb_strlen') ? mb_strlen($string, '8bit') : strlen($string); - } - - /** - * Returns the portion of string specified by the start and length parameters. - * If available uses the multibyte string function mb_substr - * @param string $string the input string. Must be one character or longer. - * @param int $start the starting position - * @param int $length the desired portion length - * @return string the extracted part of string, or FALSE on failure or an empty string. - */ - private function substr($string, $start, $length) - { - return function_exists('mb_substr') ? mb_substr($string, $start, $length, '8bit') : substr($string, $start, $length); - } -} diff --git a/framework/base/Theme.php b/framework/base/Theme.php index 03f8f55..88ecb0a 100644 --- a/framework/base/Theme.php +++ b/framework/base/Theme.php @@ -1,9 +1,7 @@ <?php /** - * Theme class file. - * * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ @@ -11,7 +9,7 @@ namespace yii\base; use Yii; use yii\base\InvalidConfigException; -use yii\util\FileHelper; +use yii\helpers\FileHelper; /** * Theme represents an application theme. @@ -42,7 +40,8 @@ class Theme extends Component /** * @var array the mapping between view directories and their corresponding themed versions. * If not set, it will be initialized as a mapping from [[Application::basePath]] to [[basePath]]. - * This property is used by [[apply()]] when a view is trying to apply the theme. + * This property is used by [[applyTo()]] when a view is trying to apply the theme. + * Path aliases can be used when specifying directories. */ public $pathMap; @@ -58,14 +57,16 @@ class Theme extends Component if (empty($this->pathMap)) { if ($this->basePath !== null) { $this->basePath = FileHelper::ensureDirectory($this->basePath); - $this->pathMap = array(Yii::$application->getBasePath() => $this->basePath); + $this->pathMap = array(Yii::$app->getBasePath() => $this->basePath); } else { throw new InvalidConfigException("Theme::basePath must be set."); } } $paths = array(); foreach ($this->pathMap as $from => $to) { - $paths[FileHelper::normalizePath($from) . DIRECTORY_SEPARATOR] = FileHelper::normalizePath($to) . DIRECTORY_SEPARATOR; + $from = FileHelper::normalizePath(Yii::getAlias($from)); + $to = FileHelper::normalizePath(Yii::getAlias($to)); + $paths[$from . DIRECTORY_SEPARATOR] = $to . DIRECTORY_SEPARATOR; } $this->pathMap = $paths; } @@ -95,7 +96,7 @@ class Theme extends Component * @param string $path the file to be themed * @return string the themed file, or the original file if the themed version is not available. */ - public function apply($path) + public function applyTo($path) { $path = FileHelper::normalizePath($path); foreach ($this->pathMap as $from => $to) { diff --git a/framework/base/UnknownMethodException.php b/framework/base/UnknownMethodException.php index 459f791..29bedca 100644 --- a/framework/base/UnknownMethodException.php +++ b/framework/base/UnknownMethodException.php @@ -1,9 +1,7 @@ <?php /** - * UnknownMethodException class file. - * * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ @@ -15,14 +13,14 @@ namespace yii\base; * @author Qiang Xue <qiang.xue@gmail.com> * @since 2.0 */ -class UnknownMethodException extends \Exception +class UnknownMethodException extends Exception { /** * @return string the user-friendly name of this exception */ public function getName() { - return \Yii::t('yii', 'Unknown Method'); + return \Yii::t('yii|Unknown Method'); } } diff --git a/framework/base/UnknownPropertyException.php b/framework/base/UnknownPropertyException.php index de8de1c..5ec3814 100644 --- a/framework/base/UnknownPropertyException.php +++ b/framework/base/UnknownPropertyException.php @@ -1,9 +1,7 @@ <?php /** - * UnknownPropertyException class file. - * * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ @@ -15,14 +13,14 @@ namespace yii\base; * @author Qiang Xue <qiang.xue@gmail.com> * @since 2.0 */ -class UnknownPropertyException extends \Exception +class UnknownPropertyException extends Exception { /** * @return string the user-friendly name of this exception */ public function getName() { - return \Yii::t('yii', 'Unknown Property'); + return \Yii::t('yii|Unknown Property'); } } diff --git a/framework/base/UrlManager.php b/framework/base/UrlManager.php deleted file mode 100644 index 3de8807..0000000 --- a/framework/base/UrlManager.php +++ /dev/null @@ -1,837 +0,0 @@ -<?php -/** - * UrlManager class file - * - * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC - * @license http://www.yiiframework.com/license/ - */ - -namespace yii\base; - -use \yii\base\Component; - -/** - * UrlManager manages the URLs of Yii applications. - * - * It provides URL construction ({@link createUrl()}) as well as parsing ({@link parseUrl()}) functionality. - * - * URLs managed via UrlManager can be in one of the following two formats, - * by setting {@link setUrlFormat urlFormat} property: - * <ul> - * <li>'path' format: /path/to/EntryScript.php/name1/value1/name2/value2...</li> - * <li>'get' format: /path/to/EntryScript.php?name1=value1&name2=value2...</li> - * </ul> - * - * When using 'path' format, UrlManager uses a set of {@link setRules rules} to: - * <ul> - * <li>parse the requested URL into a route ('ControllerID/ActionID') and GET parameters;</li> - * <li>create URLs based on the given route and GET parameters.</li> - * </ul> - * - * A rule consists of a route and a pattern. The latter is used by UrlManager to determine - * which rule is used for parsing/creating URLs. A pattern is meant to match the path info - * part of a URL. It may contain named parameters using the syntax '<ParamName:RegExp>'. - * - * When parsing a URL, a matching rule will extract the named parameters from the path info - * and put them into the $_GET variable; when creating a URL, a matching rule will extract - * the named parameters from $_GET and put them into the path info part of the created URL. - * - * If a pattern ends with '/*', it means additional GET parameters may be appended to the path - * info part of the URL; otherwise, the GET parameters can only appear in the query string part. - * - * To specify URL rules, set the {@link setRules rules} property as an array of rules (pattern=>route). - * For example, - * <pre> - * array( - * 'articles'=>'article/list', - * 'article/<id:\d+>/*'=>'article/read', - * ) - * </pre> - * Two rules are specified in the above: - * <ul> - * <li>The first rule says that if the user requests the URL '/path/to/index.php/articles', - * it should be treated as '/path/to/index.php/article/list'; and vice versa applies - * when constructing such a URL.</li> - * <li>The second rule contains a named parameter 'id' which is specified using - * the <ParamName:RegExp> syntax. It says that if the user requests the URL - * '/path/to/index.php/article/13', it should be treated as '/path/to/index.php/article/read?id=13'; - * and vice versa applies when constructing such a URL.</li> - * </ul> - * - * The route part may contain references to named parameters defined in the pattern part. - * This allows a rule to be applied to different routes based on matching criteria. - * For example, - * <pre> - * array( - * '<_c:(post|comment)>/<id:\d+>/<_a:(create|update|delete)>'=>'<_c>/<_a>', - * '<_c:(post|comment)>/<id:\d+>'=>'<_c>/view', - * '<_c:(post|comment)>s/*'=>'<_c>/list', - * ) - * </pre> - * In the above, we use two named parameters '<_c>' and '<_a>' in the route part. The '<_c>' - * parameter matches either 'post' or 'comment', while the '<_a>' parameter matches an action ID. - * - * Like normal rules, these rules can be used for both parsing and creating URLs. - * For example, using the rules above, the URL '/index.php/post/123/create' - * would be parsed as the route 'post/create' with GET parameter 'id' being 123. - * And given the route 'post/list' and GET parameter 'page' being 2, we should get a URL - * '/index.php/posts/page/2'. - * - * It is also possible to include hostname into the rules for parsing and creating URLs. - * One may extract part of the hostname to be a GET parameter. - * For example, the URL <code>http://admin.example.com/en/profile</code> may be parsed into GET parameters - * <code>user=admin</code> and <code>lang=en</code>. On the other hand, rules with hostname may also be used to - * create URLs with parameterized hostnames. - * - * In order to use parameterized hostnames, simply declare URL rules with host info, e.g.: - * <pre> - * array( - * 'http://<user:\w+>.example.com/<lang:\w+>/profile' => 'user/profile', - * ) - * </pre> - * - * If you want to customize URL generation and parsing you can write custom - * URL rule classes and use them for one or several URL rules. For example, - * <pre> - * array( - * // a standard rule - * '<action:(login|logout)>' => 'site/<action>', - * // a custom rule using data in DB - * array( - * 'class' => '\application\components\MyUrlRule', - * 'connectionID' => 'db', - * ), - * ) - * </pre> - * Please note that the custom URL rule class should extend from {@link BaseUrlRule} and - * implement the following two methods, - * <ul> - * <li>{@link BaseUrlRule::createUrl()}</li> - * <li>{@link BaseUrlRule::parseUrl()}</li> - * </ul> - * - * UrlManager is a default application component that may be accessed via - * {@link \Yii::$application->urlManager}. - * - * @property string $baseUrl The base URL of the application (the part after host name and before query string). - * If {@link showScriptName} is true, it will include the script name part. - * Otherwise, it will not, and the ending slashes are stripped off. - * @property string $urlFormat The URL format. Defaults to 'path'. Valid values include 'path' and 'get'. - * Please refer to the guide for more details about the difference between these two formats. - * - * @author Qiang Xue <qiang.xue@gmail.com> - * @since 2.0 - */ -class UrlManager extends Component -{ - const CACHE_KEY='Yii.UrlManager.rules'; - const GET_FORMAT='get'; - const PATH_FORMAT='path'; - - /** - * @var array the URL rules (pattern=>route). - */ - public $rules=array(); - /** - * @var string the URL suffix used when in 'path' format. - * For example, ".html" can be used so that the URL looks like pointing to a static HTML page. Defaults to empty. - */ - public $urlSuffix=''; - /** - * @var boolean whether to show entry script name in the constructed URL. Defaults to true. - */ - public $showScriptName=true; - /** - * @var boolean whether to append GET parameters to the path info part. Defaults to true. - * This property is only effective when {@link urlFormat} is 'path' and is mainly used when - * creating URLs. When it is true, GET parameters will be appended to the path info and - * separate from each other using slashes. If this is false, GET parameters will be in query part. - */ - public $appendParams=true; - /** - * @var string the GET variable name for route. Defaults to 'r'. - */ - public $routeVar='r'; - /** - * @var boolean whether routes are case-sensitive. Defaults to true. By setting this to false, - * the route in the incoming request will be turned to lower case first before further processing. - * As a result, you should follow the convention that you use lower case when specifying - * controller mapping ({@link CWebApplication::controllerMap}) and action mapping - * ({@link CController::actions}). Also, the directory names for organizing controllers should - * be in lower case. - */ - public $caseSensitive=true; - /** - * @var boolean whether the GET parameter values should match the corresponding - * sub-patterns in a rule before using it to create a URL. Defaults to false, meaning - * a rule will be used for creating a URL only if its route and parameter names match the given ones. - * If this property is set true, then the given parameter values must also match the corresponding - * parameter sub-patterns. Note that setting this property to true will degrade performance. - * @since 1.1.0 - */ - public $matchValue=false; - /** - * @var string the ID of the cache application component that is used to cache the parsed URL rules. - * Defaults to 'cache' which refers to the primary cache application component. - * Set this property to false if you want to disable caching URL rules. - */ - public $cacheID='cache'; - /** - * @var boolean whether to enable strict URL parsing. - * This property is only effective when {@link urlFormat} is 'path'. - * If it is set true, then an incoming URL must match one of the {@link rules URL rules}. - * Otherwise, it will be treated as an invalid request and trigger a 404 HTTP exception. - * Defaults to false. - */ - public $useStrictParsing=false; - /** - * @var string the class name or path alias for the URL rule instances. Defaults to 'CUrlRule'. - * If you change this to something else, please make sure that the new class must extend from - * {@link CBaseUrlRule} and have the same constructor signature as {@link CUrlRule}. - * It must also be serializable and autoloadable. - */ - public $urlRuleClass='UrlRule'; - - private $_urlFormat=self::GET_FORMAT; - private $_rules=array(); - private $_baseUrl; - - - /** - * Initializes the application component. - */ - public function init() - { - parent::init(); - $this->processRules(); - } - - /** - * Processes the URL rules. - */ - protected function processRules() - { - if(empty($this->rules) || $this->getUrlFormat()===self::GET_FORMAT) - return; - if($this->cacheID!==false && ($cache=\Yii::$application->getComponent($this->cacheID))!==null) - { - $hash=md5(serialize($this->rules)); - if(($data=$cache->get(self::CACHE_KEY))!==false && isset($data[1]) && $data[1]===$hash) - { - $this->_rules=$data[0]; - return; - } - } - foreach($this->rules as $pattern=>$route) - $this->_rules[]=$this->createUrlRule($route,$pattern); - if(isset($cache)) - $cache->set(self::CACHE_KEY,array($this->_rules,$hash)); - } - - /** - * Adds new URL rules. - * In order to make the new rules effective, this method must be called BEFORE - * {@link CWebApplication::processRequest}. - * @param array $rules new URL rules (pattern=>route). - * @param boolean $append whether the new URL rules should be appended to the existing ones. If false, - * they will be inserted at the beginning. - */ - public function addRules($rules, $append=true) - { - if ($append) - { - foreach($rules as $pattern=>$route) - $this->_rules[]=$this->createUrlRule($route,$pattern); - } - else - { - foreach($rules as $pattern=>$route) - array_unshift($this->_rules, $this->createUrlRule($route,$pattern)); - } - } - - /** - * Creates a URL rule instance. - * The default implementation returns a CUrlRule object. - * @param mixed $route the route part of the rule. This could be a string or an array - * @param string $pattern the pattern part of the rule - * @return CUrlRule the URL rule instance - */ - protected function createUrlRule($route,$pattern) - { - if(is_array($route) && isset($route['class'])) - return $route; - else - return new $this->urlRuleClass($route,$pattern); - } - - /** - * Constructs a URL. - * @param string $route the controller and the action (e.g. article/read) - * @param array $params list of GET parameters (name=>value). Both the name and value will be URL-encoded. - * If the name is '#', the corresponding value will be treated as an anchor - * and will be appended at the end of the URL. - * @param string $ampersand the token separating name-value pairs in the URL. Defaults to '&'. - * @return string the constructed URL - */ - public function createUrl($route,$params=array(),$ampersand='&') - { - unset($params[$this->routeVar]); - foreach($params as $i=>$param) - if($param===null) - $params[$i]=''; - - if(isset($params['#'])) - { - $anchor='#'.$params['#']; - unset($params['#']); - } - else - $anchor=''; - $route=trim($route,'/'); - foreach($this->_rules as $i=>$rule) - { - if(is_array($rule)) - $this->_rules[$i]=$rule=Yii::createComponent($rule); - if(($url=$rule->createUrl($this,$route,$params,$ampersand))!==false) - { - if($rule->hasHostInfo) - return $url==='' ? '/'.$anchor : $url.$anchor; - else - return $this->getBaseUrl().'/'.$url.$anchor; - } - } - return $this->createUrlDefault($route,$params,$ampersand).$anchor; - } - - /** - * Creates a URL based on default settings. - * @param string $route the controller and the action (e.g. article/read) - * @param array $params list of GET parameters - * @param string $ampersand the token separating name-value pairs in the URL. - * @return string the constructed URL - */ - protected function createUrlDefault($route,$params,$ampersand) - { - if($this->getUrlFormat()===self::PATH_FORMAT) - { - $url=rtrim($this->getBaseUrl().'/'.$route,'/'); - if($this->appendParams) - { - $url=rtrim($url.'/'.$this->createPathInfo($params,'/','/'),'/'); - return $route==='' ? $url : $url.$this->urlSuffix; - } - else - { - if($route!=='') - $url.=$this->urlSuffix; - $query=$this->createPathInfo($params,'=',$ampersand); - return $query==='' ? $url : $url.'?'.$query; - } - } - else - { - $url=$this->getBaseUrl(); - if(!$this->showScriptName) - $url.='/'; - if($route!=='') - { - $url.='?'.$this->routeVar.'='.$route; - if(($query=$this->createPathInfo($params,'=',$ampersand))!=='') - $url.=$ampersand.$query; - } - else if(($query=$this->createPathInfo($params,'=',$ampersand))!=='') - $url.='?'.$query; - return $url; - } - } - - /** - * Parses the user request. - * @param HttpRequest $request the request application component - * @return string the route (controllerID/actionID) and perhaps GET parameters in path format. - */ - public function parseUrl($request) - { - if($this->getUrlFormat()===self::PATH_FORMAT) - { - $rawPathInfo=$request->getPathInfo(); - $pathInfo=$this->removeUrlSuffix($rawPathInfo,$this->urlSuffix); - foreach($this->_rules as $i=>$rule) - { - if(is_array($rule)) - $this->_rules[$i]=$rule=Yii::createComponent($rule); - if(($r=$rule->parseUrl($this,$request,$pathInfo,$rawPathInfo))!==false) - return isset($_GET[$this->routeVar]) ? $_GET[$this->routeVar] : $r; - } - if($this->useStrictParsing) - throw new HttpException(404,Yii::t('yii','Unable to resolve the request "{route}".', - array('{route}'=>$pathInfo))); - else - return $pathInfo; - } - else if(isset($_GET[$this->routeVar])) - return $_GET[$this->routeVar]; - else if(isset($_POST[$this->routeVar])) - return $_POST[$this->routeVar]; - else - return ''; - } - - /** - * Parses a path info into URL segments and saves them to $_GET and $_REQUEST. - * @param string $pathInfo path info - */ - public function parsePathInfo($pathInfo) - { - if($pathInfo==='') - return; - $segs=explode('/',$pathInfo.'/'); - $n=count($segs); - for($i=0;$i<$n-1;$i+=2) - { - $key=$segs[$i]; - if($key==='') continue; - $value=$segs[$i+1]; - if(($pos=strpos($key,'['))!==false && ($m=preg_match_all('/\[(.*?)\]/',$key,$matches))>0) - { - $name=substr($key,0,$pos); - for($j=$m-1;$j>=0;--$j) - { - if($matches[1][$j]==='') - $value=array($value); - else - $value=array($matches[1][$j]=>$value); - } - if(isset($_GET[$name]) && is_array($_GET[$name])) - $value=CMap::mergeArray($_GET[$name],$value); - $_REQUEST[$name]=$_GET[$name]=$value; - } - else - $_REQUEST[$key]=$_GET[$key]=$value; - } - } - - /** - * Creates a path info based on the given parameters. - * @param array $params list of GET parameters - * @param string $equal the separator between name and value - * @param string $ampersand the separator between name-value pairs - * @param string $key this is used internally. - * @return string the created path info - */ - public function createPathInfo($params,$equal,$ampersand, $key=null) - { - $pairs = array(); - foreach($params as $k => $v) - { - if ($key!==null) - $k = $key.'['.$k.']'; - - if (is_array($v)) - $pairs[]=$this->createPathInfo($v,$equal,$ampersand, $k); - else - $pairs[]=urlencode($k).$equal.urlencode($v); - } - return implode($ampersand,$pairs); - } - - /** - * Removes the URL suffix from path info. - * @param string $pathInfo path info part in the URL - * @param string $urlSuffix the URL suffix to be removed - * @return string path info with URL suffix removed. - */ - public function removeUrlSuffix($pathInfo,$urlSuffix) - { - if($urlSuffix!=='' && substr($pathInfo,-strlen($urlSuffix))===$urlSuffix) - return substr($pathInfo,0,-strlen($urlSuffix)); - else - return $pathInfo; - } - - /** - * Returns the base URL of the application. - * @return string the base URL of the application (the part after host name and before query string). - * If {@link showScriptName} is true, it will include the script name part. - * Otherwise, it will not, and the ending slashes are stripped off. - */ - public function getBaseUrl() - { - if($this->_baseUrl!==null) - return $this->_baseUrl; - else - { - if($this->showScriptName) - $this->_baseUrl=\Yii::$application->getRequest()->getScriptUrl(); - else - $this->_baseUrl=\Yii::$application->getRequest()->getBaseUrl(); - return $this->_baseUrl; - } - } - - /** - * Sets the base URL of the application (the part after host name and before query string). - * This method is provided in case the {@link baseUrl} cannot be determined automatically. - * The ending slashes should be stripped off. And you are also responsible to remove the script name - * if you set {@link showScriptName} to be false. - * @param string $value the base URL of the application - */ - public function setBaseUrl($value) - { - $this->_baseUrl=$value; - } - - /** - * Returns the URL format. - * @return string the URL format. Defaults to 'path'. Valid values include 'path' and 'get'. - * Please refer to the guide for more details about the difference between these two formats. - */ - public function getUrlFormat() - { - return $this->_urlFormat; - } - - /** - * Sets the URL format. - * @param string $value the URL format. It must be either 'path' or 'get'. - */ - public function setUrlFormat($value) - { - if($value===self::PATH_FORMAT || $value===self::GET_FORMAT) - $this->_urlFormat=$value; - else - throw new CException(Yii::t('yii','CUrlManager.UrlFormat must be either "path" or "get".')); - } -} - - -/** - * CBaseUrlRule is the base class for a URL rule class. - * - * Custom URL rule classes should extend from this class and implement two methods: - * {@link createUrl} and {@link parseUrl}. - * - * @author Qiang Xue <qiang.xue@gmail.com> - */ -abstract class CBaseUrlRule extends CComponent -{ - /** - * @var boolean whether this rule will also parse the host info part. Defaults to false. - */ - public $hasHostInfo=false; - /** - * Creates a URL based on this rule. - * @param CUrlManager $manager the manager - * @param string $route the route - * @param array $params list of parameters (name=>value) associated with the route - * @param string $ampersand the token separating name-value pairs in the URL. - * @return mixed the constructed URL. False if this rule does not apply. - */ - abstract public function createUrl($manager,$route,$params,$ampersand); - /** - * Parses a URL based on this rule. - * @param UrlManager $manager the URL manager - * @param HttpRequest $request the request object - * @param string $pathInfo path info part of the URL (URL suffix is already removed based on {@link CUrlManager::urlSuffix}) - * @param string $rawPathInfo path info that contains the potential URL suffix - * @return mixed the route that consists of the controller ID and action ID. False if this rule does not apply. - */ - abstract public function parseUrl($manager,$request,$pathInfo,$rawPathInfo); -} - -/** - * CUrlRule represents a URL formatting/parsing rule. - * - * It mainly consists of two parts: route and pattern. The former classifies - * the rule so that it only applies to specific controller-action route. - * The latter performs the actual formatting and parsing role. The pattern - * may have a set of named parameters. - * - * @author Qiang Xue <qiang.xue@gmail.com> - */ -class CUrlRule extends CBaseUrlRule -{ - /** - * @var string the URL suffix used for this rule. - * For example, ".html" can be used so that the URL looks like pointing to a static HTML page. - * Defaults to null, meaning using the value of {@link CUrlManager::urlSuffix}. - */ - public $urlSuffix; - /** - * @var boolean whether the rule is case sensitive. Defaults to null, meaning - * using the value of {@link CUrlManager::caseSensitive}. - */ - public $caseSensitive; - /** - * @var array the default GET parameters (name=>value) that this rule provides. - * When this rule is used to parse the incoming request, the values declared in this property - * will be injected into $_GET. - */ - public $defaultParams=array(); - /** - * @var boolean whether the GET parameter values should match the corresponding - * sub-patterns in the rule when creating a URL. Defaults to null, meaning using the value - * of {@link CUrlManager::matchValue}. When this property is false, it means - * a rule will be used for creating a URL if its route and parameter names match the given ones. - * If this property is set true, then the given parameter values must also match the corresponding - * parameter sub-patterns. Note that setting this property to true will degrade performance. - */ - public $matchValue; - /** - * @var string the HTTP verb (e.g. GET, POST, DELETE) that this rule should match. - * If this rule can match multiple verbs, please separate them with commas. - * If this property is not set, the rule can match any verb. - * Note that this property is only used when parsing a request. It is ignored for URL creation. - */ - public $verb; - /** - * @var boolean whether this rule is only used for request parsing. - * Defaults to false, meaning the rule is used for both URL parsing and creation. - */ - public $parsingOnly=false; - /** - * @var string the controller/action pair - */ - public $route; - /** - * @var array the mapping from route param name to token name (e.g. _r1=><1>) - */ - public $references=array(); - /** - * @var string the pattern used to match route - */ - public $routePattern; - /** - * @var string regular expression used to parse a URL - */ - public $pattern; - /** - * @var string template used to construct a URL - */ - public $template; - /** - * @var array list of parameters (name=>regular expression) - */ - public $params=array(); - /** - * @var boolean whether the URL allows additional parameters at the end of the path info. - */ - public $append; - /** - * @var boolean whether host info should be considered for this rule - */ - public $hasHostInfo; - - /** - * Constructor. - * @param string $route the route of the URL (controller/action) - * @param string $pattern the pattern for matching the URL - */ - public function __construct($route,$pattern) - { - if(is_array($route)) - { - foreach(array('urlSuffix', 'caseSensitive', 'defaultParams', 'matchValue', 'verb', 'parsingOnly') as $name) - { - if(isset($route[$name])) - $this->$name=$route[$name]; - } - if(isset($route['pattern'])) - $pattern=$route['pattern']; - $route=$route[0]; - } - $this->route=trim($route,'/'); - - $tr2['/']=$tr['/']='\\/'; - - if(strpos($route,'<')!==false && preg_match_all('/<(\w+)>/',$route,$matches2)) - { - foreach($matches2[1] as $name) - $this->references[$name]="<$name>"; - } - - $this->hasHostInfo=!strncasecmp($pattern,'http://',7) || !strncasecmp($pattern,'https://',8); - - if($this->verb!==null) - $this->verb=preg_split('/[\s,]+/',strtoupper($this->verb),-1,PREG_SPLIT_NO_EMPTY); - - if(preg_match_all('/<(\w+):?(.*?)?>/',$pattern,$matches)) - { - $tokens=array_combine($matches[1],$matches[2]); - foreach($tokens as $name=>$value) - { - if($value==='') - $value='[^\/]+'; - $tr["<$name>"]="(?P<$name>$value)"; - if(isset($this->references[$name])) - $tr2["<$name>"]=$tr["<$name>"]; - else - $this->params[$name]=$value; - } - } - $p=rtrim($pattern,'*'); - $this->append=$p!==$pattern; - $p=trim($p,'/'); - $this->template=preg_replace('/<(\w+):?.*?>/','<$1>',$p); - $this->pattern='/^'.strtr($this->template,$tr).'\/'; - if($this->append) - $this->pattern.='/u'; - else - $this->pattern.='$/u'; - - if($this->references!==array()) - $this->routePattern='/^'.strtr($this->route,$tr2).'$/u'; - - if(YII_DEBUG && @preg_match($this->pattern,'test')===false) - throw new CException(Yii::t('yii','The URL pattern "{pattern}" for route "{route}" is not a valid regular expression.', - array('{route}'=>$route,'{pattern}'=>$pattern))); - } - - /** - * Creates a URL based on this rule. - * @param CUrlManager $manager the manager - * @param string $route the route - * @param array $params list of parameters - * @param string $ampersand the token separating name-value pairs in the URL. - * @return mixed the constructed URL or false on error - */ - public function createUrl($manager,$route,$params,$ampersand) - { - if($this->parsingOnly) - return false; - - if($manager->caseSensitive && $this->caseSensitive===null || $this->caseSensitive) - $case=''; - else - $case='i'; - - $tr=array(); - if($route!==$this->route) - { - if($this->routePattern!==null && preg_match($this->routePattern.$case,$route,$matches)) - { - foreach($this->references as $key=>$name) - $tr[$name]=$matches[$key]; - } - else - return false; - } - - foreach($this->defaultParams as $key=>$value) - { - if(isset($params[$key])) - { - if($params[$key]==$value) - unset($params[$key]); - else - return false; - } - } - - foreach($this->params as $key=>$value) - if(!isset($params[$key])) - return false; - - if($manager->matchValue && $this->matchValue===null || $this->matchValue) - { - foreach($this->params as $key=>$value) - { - if(!preg_match('/\A'.$value.'\z/u'.$case,$params[$key])) - return false; - } - } - - foreach($this->params as $key=>$value) - { - $tr["<$key>"]=urlencode($params[$key]); - unset($params[$key]); - } - - $suffix=$this->urlSuffix===null ? $manager->urlSuffix : $this->urlSuffix; - - $url=strtr($this->template,$tr); - - if($this->hasHostInfo) - { - $hostInfo=\Yii::$application->getRequest()->getHostInfo(); - if(stripos($url,$hostInfo)===0) - $url=substr($url,strlen($hostInfo)); - } - - if(empty($params)) - return $url!=='' ? $url.$suffix : $url; - - if($this->append) - $url.='/'.$manager->createPathInfo($params,'/','/').$suffix; - else - { - if($url!=='') - $url.=$suffix; - $url.='?'.$manager->createPathInfo($params,'=',$ampersand); - } - - return $url; - } - - /** - * Parses a URL based on this rule. - * @param UrlManager $manager the URL manager - * @param HttpRequest $request the request object - * @param string $pathInfo path info part of the URL - * @param string $rawPathInfo path info that contains the potential URL suffix - * @return mixed the route that consists of the controller ID and action ID or false on error - */ - public function parseUrl($manager,$request,$pathInfo,$rawPathInfo) - { - if($this->verb!==null && !in_array($request->getRequestType(), $this->verb, true)) - return false; - - if($manager->caseSensitive && $this->caseSensitive===null || $this->caseSensitive) - $case=''; - else - $case='i'; - - if($this->urlSuffix!==null) - $pathInfo=$manager->removeUrlSuffix($rawPathInfo,$this->urlSuffix); - - // URL suffix required, but not found in the requested URL - if($manager->useStrictParsing && $pathInfo===$rawPathInfo) - { - $urlSuffix=$this->urlSuffix===null ? $manager->urlSuffix : $this->urlSuffix; - if($urlSuffix!='' && $urlSuffix!=='/') - return false; - } - - if($this->hasHostInfo) - $pathInfo=strtolower($request->getHostInfo()).rtrim('/'.$pathInfo,'/'); - - $pathInfo.='/'; - - if(preg_match($this->pattern.$case,$pathInfo,$matches)) - { - foreach($this->defaultParams as $name=>$value) - { - if(!isset($_GET[$name])) - $_REQUEST[$name]=$_GET[$name]=$value; - } - $tr=array(); - foreach($matches as $key=>$value) - { - if(isset($this->references[$key])) - $tr[$this->references[$key]]=$value; - else if(isset($this->params[$key])) - $_REQUEST[$key]=$_GET[$key]=$value; - } - if($pathInfo!==$matches[0]) // there're additional GET params - $manager->parsePathInfo(ltrim(substr($pathInfo,strlen($matches[0])),'/')); - if($this->routePattern!==null) - return strtr($this->route,$tr); - else - return $this->route; - } - else - return false; - } -} \ No newline at end of file diff --git a/framework/base/UserException.php b/framework/base/UserException.php new file mode 100644 index 0000000..01ca602 --- /dev/null +++ b/framework/base/UserException.php @@ -0,0 +1,19 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\base; + +/** + * UserException is the base class for exceptions that are meant to be shown to end users. + * Such exceptions are often caused by mistakes of end users. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class UserException extends Exception +{ +} diff --git a/framework/base/Vector.php b/framework/base/Vector.php index c271ccc..7d43fdb 100644 --- a/framework/base/Vector.php +++ b/framework/base/Vector.php @@ -1,9 +1,7 @@ <?php /** - * Vector class file. - * * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ @@ -101,7 +99,7 @@ class Vector extends Object implements \IteratorAggregate, \ArrayAccess, \Counta * Returns the item at the specified index. * @param integer $index the index of the item * @return mixed the item at the index - * @throws InvalidCallException if the index is out of range + * @throws InvalidParamException if the index is out of range */ public function itemAt($index) { @@ -110,7 +108,7 @@ class Vector extends Object implements \IteratorAggregate, \ArrayAccess, \Counta } elseif ($index >= 0 && $index < $this->_c) { // in case the value is null return $this->_d[$index]; } else { - throw new InvalidCallException('Index out of range: ' . $index); + throw new InvalidParamException('Index out of range: ' . $index); } } @@ -132,7 +130,7 @@ class Vector extends Object implements \IteratorAggregate, \ArrayAccess, \Counta * one step towards the end. * @param integer $index the specified position. * @param mixed $item new item to be inserted into the vector - * @throws InvalidCallException if the index specified is out of range, or the vector is read-only. + * @throws InvalidParamException if the index specified is out of range, or the vector is read-only. */ public function insertAt($index, $item) { @@ -142,7 +140,7 @@ class Vector extends Object implements \IteratorAggregate, \ArrayAccess, \Counta array_splice($this->_d, $index, 0, array($item)); $this->_c++; } else { - throw new InvalidCallException('Index out of range: ' . $index); + throw new InvalidParamException('Index out of range: ' . $index); } } @@ -169,7 +167,7 @@ class Vector extends Object implements \IteratorAggregate, \ArrayAccess, \Counta * Removes an item at the specified position. * @param integer $index the index of the item to be removed. * @return mixed the removed item. - * @throws InvalidCallException if the index is out of range, or the vector is read only. + * @throws InvalidParamException if the index is out of range, or the vector is read only. */ public function removeAt($index) { @@ -183,7 +181,7 @@ class Vector extends Object implements \IteratorAggregate, \ArrayAccess, \Counta return $item; } } else { - throw new InvalidCallException('Index out of range: ' . $index); + throw new InvalidParamException('Index out of range: ' . $index); } } @@ -193,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) { @@ -211,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; } @@ -242,13 +240,13 @@ class Vector extends Object implements \IteratorAggregate, \ArrayAccess, \Counta * Copies iterable data into the vector. * Note, existing data in the vector will be cleared first. * @param mixed $data the data to be copied from, must be an array or an object implementing `Traversable` - * @throws InvalidCallException if data is neither an array nor an object implementing `Traversable`. + * @throws InvalidParamException if data is neither an array nor an object implementing `Traversable`. */ public function copyFrom($data) { if (is_array($data) || $data instanceof \Traversable) { if ($this->_c > 0) { - $this->clear(); + $this->removeAll(); } if ($data instanceof self) { $data = $data->_d; @@ -257,7 +255,7 @@ class Vector extends Object implements \IteratorAggregate, \ArrayAccess, \Counta $this->add($item); } } else { - throw new InvalidCallException('Data must be either an array or an object implementing Traversable.'); + throw new InvalidParamException('Data must be either an array or an object implementing Traversable.'); } } @@ -265,7 +263,7 @@ class Vector extends Object implements \IteratorAggregate, \ArrayAccess, \Counta * Merges iterable data into the vector. * New items will be appended to the end of the existing items. * @param array|\Traversable $data the data to be merged with. It must be an array or object implementing Traversable - * @throws InvalidCallException if data is neither an array nor an object implementing `Traversable`. + * @throws InvalidParamException if data is neither an array nor an object implementing `Traversable`. */ public function mergeWith($data) { @@ -277,7 +275,7 @@ class Vector extends Object implements \IteratorAggregate, \ArrayAccess, \Counta $this->add($item); } } else { - throw new InvalidCallException('The data to be merged with must be an array or an object implementing Traversable.'); + throw new InvalidParamException('The data to be merged with must be an array or an object implementing Traversable.'); } } diff --git a/framework/base/VectorIterator.php b/framework/base/VectorIterator.php index d1fefad..f83d42d 100644 --- a/framework/base/VectorIterator.php +++ b/framework/base/VectorIterator.php @@ -1,9 +1,7 @@ <?php /** - * VectorIterator class file. - * * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ diff --git a/framework/base/View.php b/framework/base/View.php index 410e3c5..c7087c1 100644 --- a/framework/base/View.php +++ b/framework/base/View.php @@ -1,23 +1,21 @@ <?php /** - * View class file. - * * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ namespace yii\base; use Yii; -use yii\util\FileHelper; use yii\base\Application; +use yii\helpers\FileHelper; /** * View represents a view object in the MVC pattern. - * + * * View provides a set of methods (e.g. [[render()]]) for rendering purpose. - * + * * @author Qiang Xue <qiang.xue@gmail.com> * @since 2.0 */ @@ -26,134 +24,124 @@ class View extends Component /** * @var object the object that owns this view. This can be a controller, a widget, or any other object. */ - public $owner; + public $context; /** - * @var string the layout to be applied when [[render()]] or [[renderContent()]] is called. - * If not set, it will use the [[Module::layout]] of the currently active module. + * @var mixed custom parameters that are shared among view templates. */ - public $layout; + public $params; /** - * @var string the language that the view should be rendered in. If not set, it will use - * the value of [[Application::language]]. + * @var ViewRenderer|array the view renderer object or the configuration array for + * creating the view renderer. If not set, view files will be treated as normal PHP files. */ - public $language; + public $renderer; /** - * @var string the language that the original view is in. If not set, it will use - * the value of [[Application::sourceLanguage]]. + * @var Theme|array the theme object or the configuration array for creating the theme. + * If not set, it means theming is not enabled. */ - public $sourceLanguage; + public $theme; /** - * @var boolean whether to localize the view when possible. Defaults to true. - * Note that when this is true, if a localized view cannot be found, the original view will be rendered. - * No error will be reported. + * @var array a list of named output clips. You can call [[beginClip()]] and [[endClip()]] + * to capture small fragments of a view. They can be later accessed at somewhere else + * through this property. */ - public $enableI18N = true; + public $clips; /** - * @var boolean whether to theme the view when possible. Defaults to true. - * Note that theming will be disabled if [[Application::theme]] is not set. + * @var Widget[] the widgets that are currently being rendered (not ended). This property + * is maintained by [[beginWidget()]] and [[endWidget()]] methods. Do not modify it directly. */ - public $enableTheme = true; + public $widgetStack = array(); /** - * @var mixed custom parameters that are available in the view template + * @var array a list of currently active fragment cache widgets. This property + * is used internally to implement the content caching feature. Do not modify it. */ - public $params; - + public $cacheStack = array(); /** - * @var Widget[] the widgets that are currently not ended + * @var array a list of placeholders for embedding dynamic contents. This property + * is used internally to implement the content caching feature. Do not modify it. */ - private $_widgetStack = array(); + public $dynamicPlaceholders = array(); - /** - * Constructor. - * @param object $owner the owner of this view. This usually is a controller or a widget. - * @param array $config name-value pairs that will be used to initialize the object properties - */ - public function __construct($owner, $config = array()) - { - $this->owner = $owner; - parent::__construct($config); - } - - /** - * Renders a view within a layout. - * This method is similar to [[renderPartial()]] except that if a layout is available, - * this method will embed the view result into the layout and then return it. - * @param string $view the view to be rendered. Please refer to [[findViewFile()]] on possible formats of the view name. - * @param array $params the parameters that should be made available in the view. The PHP function `extract()` - * will be called on this variable to extract the variables from this parameter. - * @return string the rendering result - * @throws InvalidConfigException if the view file or layout file cannot be found - * @see findViewFile() - * @see findLayoutFile() - */ - public function render($view, $params = array()) - { - $content = $this->renderPartial($view, $params); - return $this->renderContent($content); - } /** - * Renders a text content within a layout. - * The layout being used is resolved by [[findLayout()]]. - * If no layout is available, the content will be returned back. - * @param string $content the content to be rendered - * @return string the rendering result - * @throws InvalidConfigException if the layout file cannot be found - * @see findLayoutFile() + * Initializes the view component. */ - public function renderContent($content) + public function init() { - $layoutFile = $this->findLayoutFile(); - if ($layoutFile !== false) { - return $this->renderFile($layoutFile, array('content' => $content)); - } else { - return $content; + parent::init(); + if (is_array($this->renderer)) { + $this->renderer = Yii::createObject($this->renderer); + } + if (is_array($this->theme)) { + $this->theme = Yii::createObject($this->theme); } } /** * Renders a view. * - * The method first finds the actual view file corresponding to the specified view. - * It then calls [[renderFile()]] to render the view file. The rendering result is returned - * as a string. If the view file does not exist, an exception will be thrown. + * This method will call [[findViewFile()]] to convert the view name into the corresponding view + * file path, and it will then call [[renderFile()]] to render the view. * - * @param string $view the view to be rendered. Please refer to [[findViewFile()]] on possible formats of the view name. - * @param array $params the parameters that should be made available in the view. The PHP function `extract()` - * will be called on this variable to extract the variables from this parameter. + * @param string $view the view name. Please refer to [[findViewFile()]] on how to specify this parameter. + * @param array $params the parameters (name-value pairs) that will be extracted and made available in the view file. + * @param object $context the context that the view should use for rendering the view. If null, + * existing [[context]] will be used. * @return string the rendering result - * @throws InvalidCallException if the view file cannot be found - * @see findViewFile() + * @throws InvalidParamException if the view cannot be resolved or the view file does not exist. + * @see renderFile + * @see findViewFile */ - public function renderPartial($view, $params = array()) + public function render($view, $params = array(), $context = null) { - $file = $this->findViewFile($view); - if ($file !== false) { - return $this->renderFile($file, $params); - } else { - throw new InvalidCallException("Unable to find the view file for view '$view'."); - } + $viewFile = $this->findViewFile($context, $view); + return $this->renderFile($viewFile, $params, $context); } /** * Renders a view file. * - * If a [[ViewRenderer|view renderer]] is installed, this method will try to use the view renderer - * to render the view file. Otherwise, it will simply include the view file, capture its output - * and return it as a string. + * If [[theme]] is enabled (not null), it will try to render the themed version of the view file as long + * as it is available. * - * @param string $file the view file. + * The method will call [[FileHelper::localize()]] to localize the view file. + * + * If [[renderer]] is enabled (not null), the method will use it to render the view file. + * Otherwise, it will simply include the view file as a normal PHP file, capture its output and + * return it as a string. + * + * @param string $viewFile the view file. This can be either a file path or a path alias. * @param array $params the parameters (name-value pairs) that will be extracted and made available in the view file. + * @param object $context the context that the view should use for rendering the view. If null, + * existing [[context]] will be used. * @return string the rendering result + * @throws InvalidParamException if the view file does not exist */ - public function renderFile($file, $params = array()) + public function renderFile($viewFile, $params = array(), $context = null) { - $renderer = Yii::$application->getViewRenderer(); - if ($renderer !== null) { - return $renderer->render($this, $file, $params); + $viewFile = Yii::getAlias($viewFile); + if (is_file($viewFile)) { + if ($this->theme !== null) { + $viewFile = $this->theme->applyTo($viewFile); + } + $viewFile = FileHelper::localize($viewFile); } else { - return $this->renderPhpFile($file, $params); + throw new InvalidParamException("The view file does not exist: $viewFile"); } + + $oldContext = $this->context; + if ($context !== null) { + $this->context = $context; + } + + if ($this->renderer !== null) { + $output = $this->renderer->render($this, $viewFile, $params); + } else { + $output = $this->renderPhpFile($viewFile, $params); + } + + $this->context = $oldContext; + + return $output; } /** @@ -163,6 +151,8 @@ class View extends Component * It extracts the given parameters and makes them available in the view file. * The method captures the output of the included view file and returns it as a string. * + * This method should mainly be called by view renderer or [[renderFile()]]. + * * @param string $_file_ the view file. * @param array $_params_ the parameters (name-value pairs) that will be extracted and made available in the view file. * @return string the rendering result @@ -177,6 +167,95 @@ class View extends Component } /** + * Renders dynamic content returned by the given PHP statements. + * This method is mainly used together with content caching (fragment caching and page caching) + * when some portions of the content (called *dynamic content*) should not be cached. + * The dynamic content must be returned by some PHP statements. + * @param string $statements the PHP statements for generating the dynamic content. + * @return string the placeholder of the dynamic content, or the dynamic content if there is no + * active content cache currently. + */ + public function renderDynamic($statements) + { + if (!empty($this->cacheStack)) { + $n = count($this->dynamicPlaceholders); + $placeholder = "<![CDATA[YDP-$n]]>"; + $this->addDynamicPlaceholder($placeholder, $statements); + return $placeholder; + } else { + return $this->evaluateDynamicContent($statements); + } + } + + /** + * Adds a placeholder for dynamic content. + * This method is internally used. + * @param string $placeholder the placeholder name + * @param string $statements the PHP statements for generating the dynamic content + */ + public function addDynamicPlaceholder($placeholder, $statements) + { + foreach ($this->cacheStack as $cache) { + $cache->dynamicPlaceholders[$placeholder] = $statements; + } + $this->dynamicPlaceholders[$placeholder] = $statements; + } + + /** + * Evaluates the given PHP statements. + * This method is mainly used internally to implement dynamic content feature. + * @param string $statements the PHP statements to be evaluated. + * @return mixed the return value of the PHP statements. + */ + public function evaluateDynamicContent($statements) + { + return eval($statements); + } + + /** + * Finds the view file based on the given view name. + * + * A view name can be specified in one of the following formats: + * + * - path alias (e.g. "@app/views/site/index"); + * - absolute path within application (e.g. "//site/index"): the view name starts with double slashes. + * The actual view file will be looked for under the [[Application::viewPath|view path]] of the application. + * - absolute path within module (e.g. "/site/index"): the view name starts with a single slash. + * The actual view file will be looked for under the [[Module::viewPath|view path]] of the currently + * active module. + * - relative path (e.g. "index"): the actual view file will be looked for under [[Controller::viewPath|viewPath]] + * of the context object, assuming the context is either a [[Controller]] or a [[Widget]]. + * + * If the view name does not contain a file extension, it will use the default one `.php`. + * + * @param object $context the view context object + * @param string $view the view name or the path alias of the view file. + * @return string the view file path. Note that the file may not exist. + * @throws InvalidParamException if the view file is an invalid path alias or the context cannot be + * used to determine the actual view file corresponding to the specified view. + */ + protected function findViewFile($context, $view) + { + if (strncmp($view, '@', 1) === 0) { + // e.g. "@app/views/main" + $file = Yii::getAlias($view); + } elseif (strncmp($view, '//', 2) === 0) { + // e.g. "//layouts/main" + $file = Yii::$app->getViewPath() . DIRECTORY_SEPARATOR . ltrim($view, '/'); + } elseif (strncmp($view, '/', 1) === 0) { + // e.g. "/site/index" + $file = Yii::$app->controller->module->getViewPath() . DIRECTORY_SEPARATOR . ltrim($view, '/'); + } elseif ($context instanceof Controller || $context instanceof Widget) { + /** @var $context Controller|Widget */ + $file = $context->getViewPath() . DIRECTORY_SEPARATOR . $view; + } else { + throw new InvalidParamException("Unable to resolve the view file for '$view'."); + } + + return FileHelper::getExtension($file) === '' ? $file . '.php' : $file; + } + + /** * Creates a widget. * This method will use [[Yii::createObject()]] to create the widget. * @param string $class the widget class name or path alias @@ -186,7 +265,7 @@ class View extends Component public function createWidget($class, $properties = array()) { $properties['class'] = $class; - return Yii::createObject($properties, $this->owner); + return Yii::createObject($properties, $this->context); } /** @@ -225,7 +304,7 @@ class View extends Component public function beginWidget($class, $properties = array()) { $widget = $this->createWidget($class, $properties); - $this->_widgetStack[] = $widget; + $this->widgetStack[] = $widget; return $widget; } @@ -235,260 +314,108 @@ class View extends Component * If you want to capture the rendering result of a widget, you may use * [[createWidget()]] and [[Widget::run()]]. * @return Widget the widget instance - * @throws Exception if [[beginWidget()]] and [[endWidget()]] calls are not properly nested + * @throws InvalidCallException if [[beginWidget()]] and [[endWidget()]] calls are not properly nested */ public function endWidget() { - $widget = array_pop($this->_widgetStack); + $widget = array_pop($this->widgetStack); if ($widget instanceof Widget) { $widget->run(); return $widget; } else { - throw new Exception("Unmatched beginWidget() and endWidget() calls."); + throw new InvalidCallException("Unmatched beginWidget() and endWidget() calls."); } } -// -// /** -// * Begins recording a clip. -// * This method is a shortcut to beginning [[yii\widgets\Clip]] -// * @param string $id the clip ID. -// * @param array $properties initial property values for [[yii\widgets\Clip]] -// */ -// public function beginClip($id, $properties = array()) -// { -// $properties['id'] = $id; -// $this->beginWidget('yii\widgets\Clip', $properties); -// } -// -// /** -// * Ends recording a clip. -// */ -// public function endClip() -// { -// $this->endWidget(); -// } -// -// /** -// * Begins fragment caching. -// * This method will display cached content if it is available. -// * If not, it will start caching and would expect an [[endCache()]] -// * call to end the cache and save the content into cache. -// * A typical usage of fragment caching is as follows, -// * -// * ~~~ -// * if($this->beginCache($id)) { -// * // ...generate content here -// * $this->endCache(); -// * } -// * ~~~ -// * -// * @param string $id a unique ID identifying the fragment to be cached. -// * @param array $properties initial property values for [[yii\widgets\OutputCache]] -// * @return boolean whether we need to generate content for caching. False if cached version is available. -// * @see endCache -// */ -// public function beginCache($id, $properties = array()) -// { -// $properties['id'] = $id; -// $cache = $this->beginWidget('yii\widgets\OutputCache', $properties); -// if ($cache->getIsContentCached()) { -// $this->endCache(); -// return false; -// } else { -// return true; -// } -// } -// -// /** -// * Ends fragment caching. -// * This is an alias to [[endWidget()]] -// * @see beginCache -// */ -// public function endCache() -// { -// $this->endWidget(); -// } -// -// /** -// * Begins the rendering of content that is to be decorated by the specified view. -// * @param mixed $view the name of the view that will be used to decorate the content. The actual view script -// * is resolved via {@link getViewFile}. If this parameter is null (default), -// * the default layout will be used as the decorative view. -// * Note that if the current controller does not belong to -// * any module, the default layout refers to the application's {@link CWebApplication::layout default layout}; -// * If the controller belongs to a module, the default layout refers to the module's -// * {@link CWebModule::layout default layout}. -// * @param array $params the variables (name=>value) to be extracted and made available in the decorative view. -// * @see endContent -// * @see yii\widgets\ContentDecorator -// */ -// public function beginContent($view, $params = array()) -// { -// $this->beginWidget('yii\widgets\ContentDecorator', array( -// 'view' => $view, -// 'params' => $params, -// )); -// } -// -// /** -// * Ends the rendering of content. -// * @see beginContent -// */ -// public function endContent() -// { -// $this->endWidget(); -// } /** - * Finds the view file based on the given view name. - * - * A view name can be specified in one of the following formats: - * - * - path alias (e.g. "@application/views/site/index"); - * - absolute path within application (e.g. "//site/index"): the view name starts with double slashes. - * The actual view file will be looked for under the [[Application::viewPath|view path]] of the application. - * - absolute path within module (e.g. "/site/index"): the view name starts with a single slash. - * The actual view file will be looked for under the [[Module::viewPath|view path]] of the currently - * active module. - * - relative path (e.g. "index"): the actual view file will be looked for under the [[owner]]'s view path. - * If [[owner]] is a widget or a controller, its view path is given by their `viewPath` property. - * If [[owner]] is an object of any other type, its view path is the `view` sub-directory of the directory - * containing the owner class file. - * - * If the view name does not contain a file extension, it will default to `.php`. - * - * If [[enableTheme]] is true and there is an active application them, the method will also - * attempt to use a themed version of the view file, when available. - * - * And if [[enableI18N]] is true, the method will attempt to use a translated version of the view file, - * when available. - * - * @param string $view the view name or path alias. If the view name does not specify - * the view file extension name, it will use `.php` as the extension name. - * @return string the view file path if it exists. False if the view file cannot be found. - * @throws InvalidConfigException if the view file does not exist + * Begins recording a clip. + * This method is a shortcut to beginning [[yii\widgets\Clip]] + * @param string $id the clip ID. + * @param boolean $renderInPlace whether to render the clip content in place. + * Defaults to false, meaning the captured clip will not be displayed. + * @return \yii\widgets\Clip the Clip widget instance + * @see \yii\widgets\Clip */ - public function findViewFile($view) + public function beginClip($id, $renderInPlace = false) { - if (FileHelper::getExtension($view) === '') { - $view .= '.php'; - } - if (strncmp($view, '@', 1) === 0) { - // e.g. "@application/views/common" - if (($file = Yii::getAlias($view)) === false) { - throw new InvalidConfigException("Invalid path alias: $view"); - } - } elseif (strncmp($view, '/', 1) !== 0) { - // e.g. "index" - if ($this->owner instanceof Controller || $this->owner instanceof Widget) { - $file = $this->owner->getViewPath() . DIRECTORY_SEPARATOR . $view; - } elseif ($this->owner !== null) { - $class = new \ReflectionClass($this->owner); - $file = dirname($class->getFileName()) . DIRECTORY_SEPARATOR . 'views' . DIRECTORY_SEPARATOR . $view; - } else { - $file = Yii::$application->getViewPath() . DIRECTORY_SEPARATOR . $view; - } - } elseif (strncmp($view, '//', 2) !== 0 && Yii::$application->controller !== null) { - // e.g. "/site/index" - $file = Yii::$application->controller->module->getViewPath() . DIRECTORY_SEPARATOR . ltrim($view, '/'); - } else { - // e.g. "//layouts/main" - $file = Yii::$application->getViewPath() . DIRECTORY_SEPARATOR . ltrim($view, '/'); - } + return $this->beginWidget('yii\widgets\Clip', array( + 'id' => $id, + 'renderInPlace' => $renderInPlace, + 'view' => $this, + )); + } - if (is_file($file)) { - if ($this->enableTheme && ($theme = Yii::$application->getTheme()) !== null) { - $file = $theme->apply($file); - } - return $this->enableI18N ? FileHelper::localize($file, $this->language, $this->sourceLanguage) : $file; - } else { - throw new InvalidConfigException("View file for view '$view' does not exist: $file"); - } + /** + * Ends recording a clip. + */ + public function endClip() + { + $this->endWidget(); } /** - * Finds the layout file that can be applied to the view. - * - * The applicable layout is resolved according to the following rules: - * - * - If [[layout]] is specified as a string, use it as the layout name and search for the layout file - * under the layout path of the currently active module; - * - If [[layout]] is null and [[owner]] is a controller: - * * If the controller's [[Controller::layout|layout]] is a string, use it as the layout name - * and search for the layout file under the layout path of the parent module of the controller; - * * If the controller's [[Controller::layout|layout]] is null, look through its ancestor modules - * and find the first one whose [[Module::layout|layout]] is not null. Use the layout specified - * by that module; - * - Returns false for all other cases. - * - * Like view names, a layout name can take several formats: - * - * - path alias (e.g. "@application/views/layouts/main"); - * - absolute path (e.g. "/main"): the layout name starts with a slash. The actual layout file will be - * looked for under the [[Application::layoutPath|layout path]] of the application; - * - relative path (e.g. "main"): the actual layout layout file will be looked for under the - * [[Module::viewPath|view path]] of the context module determined by the above layout resolution process. - * - * If the layout name does not contain a file extension, it will default to `.php`. - * - * If [[enableTheme]] is true and there is an active application them, the method will also - * attempt to use a themed version of the layout file, when available. + * Begins the rendering of content that is to be decorated by the specified view. + * @param string $view the name of the view that will be used to decorate the content enclosed by this widget. + * Please refer to [[View::findViewFile()]] on how to set this property. + * @param array $params the variables (name=>value) to be extracted and made available in the decorative view. + * @return \yii\widgets\ContentDecorator the ContentDecorator widget instance + * @see \yii\widgets\ContentDecorator + */ + public function beginContent($view, $params = array()) + { + return $this->beginWidget('yii\widgets\ContentDecorator', array( + 'view' => $this, + 'viewName' => $view, + 'params' => $params, + )); + } + + /** + * Ends the rendering of content. + */ + public function endContent() + { + $this->endWidget(); + } + + /** + * Begins fragment caching. + * This method will display cached content if it is available. + * If not, it will start caching and would expect an [[endCache()]] + * call to end the cache and save the content into cache. + * A typical usage of fragment caching is as follows, * - * And if [[enableI18N]] is true, the method will attempt to use a translated version of the layout file, - * when available. + * ~~~ + * if($this->beginCache($id)) { + * // ...generate content here + * $this->endCache(); + * } + * ~~~ * - * @return string|boolean the layout file path, or false if layout is not needed. - * @throws InvalidConfigException if the layout file cannot be found + * @param string $id a unique ID identifying the fragment to be cached. + * @param array $properties initial property values for [[\yii\widgets\FragmentCache]] + * @return boolean whether you should generate the content for caching. + * False if the cached version is available. */ - public function findLayoutFile() + public function beginCache($id, $properties = array()) { - /** @var $module Module */ - if (is_string($this->layout)) { - if (Yii::$application->controller) { - $module = Yii::$application->controller->module; - } else { - $module = Yii::$application; - } - $view = $this->layout; - } elseif ($this->owner instanceof Controller) { - if (is_string($this->owner->layout)) { - $module = $this->owner->module; - $view = $this->owner->layout; - } elseif ($this->owner->layout === null) { - $module = $this->owner->module; - while ($module !== null && $module->layout === null) { - $module = $module->module; - } - if ($module !== null && is_string($module->layout)) { - $view = $module->layout; - } - } - } - - if (!isset($view)) { + $properties['id'] = $id; + $properties['view'] = $this; + /** @var $cache \yii\widgets\FragmentCache */ + $cache = $this->beginWidget('yii\widgets\FragmentCache', $properties); + if ($cache->getCachedContent() !== false) { + $this->endCache(); return false; - } - - if (FileHelper::getExtension($view) === '') { - $view .= '.php'; - } - if (strncmp($view, '@', 1) === 0) { - if (($file = Yii::getAlias($view)) === false) { - throw new InvalidConfigException("Invalid path alias: $view"); - } - } elseif (strncmp($view, '/', 1) === 0) { - $file = Yii::$application->getLayoutPath() . DIRECTORY_SEPARATOR . $view; } else { - $file = $module->getLayoutPath() . DIRECTORY_SEPARATOR . $view; + return true; } + } - if (is_file($file)) { - if ($this->enableTheme && ($theme = Yii::$application->getTheme()) !== null) { - $file = $theme->apply($file); - } - return $this->enableI18N ? FileHelper::localize($file, $this->language, $this->sourceLanguage) : $file; - } else { - throw new InvalidConfigException("Layout file for layout '$view' does not exist: $file"); - } + /** + * Ends fragment caching. + */ + public function endCache() + { + $this->endWidget(); } } \ No newline at end of file diff --git a/framework/base/ViewRenderer.php b/framework/base/ViewRenderer.php index ecb216d..576bbe8 100644 --- a/framework/base/ViewRenderer.php +++ b/framework/base/ViewRenderer.php @@ -1,9 +1,7 @@ <?php /** - * ViewRenderer class file. - * * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ diff --git a/framework/base/Widget.php b/framework/base/Widget.php index bdec634..24d0685 100644 --- a/framework/base/Widget.php +++ b/framework/base/Widget.php @@ -1,14 +1,15 @@ <?php /** - * Widget class file. - * * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ namespace yii\base; +use Yii; +use yii\helpers\FileHelper; + /** * Widget is the base class for widgets. * @@ -72,35 +73,26 @@ class Widget extends Component /** * Renders a view. - * - * The method first finds the actual view file corresponding to the specified view. - * It then calls [[renderFile()]] to render the view file. The rendering result is returned - * as a string. If the view file does not exist, an exception will be thrown. - * - * To determine which view file should be rendered, the method calls [[findViewFile()]] which - * will search in the directories as specified by [[basePath]]. - * - * View name can be a path alias representing an absolute file path (e.g. `@application/views/layout/index`), - * or a path relative to [[basePath]]. The file suffix is optional and defaults to `.php` if not given - * in the view name. - * - * @param string $view the view to be rendered. This can be either a path alias or a path relative to [[basePath]]. - * @param array $params the parameters that should be made available in the view. The PHP function `extract()` - * will be called on this variable to extract the variables from this parameter. - * @return string the rendering result - * @throws Exception if the view file cannot be found + * @param string $view the view name. Please refer to [[findViewFile()]] on how to specify a view name. + * @param array $params the parameters (name-value pairs) that should be made available in the view. + * @return string the rendering result. + * @throws InvalidParamException if the view file does not exist. */ public function render($view, $params = array()) { - return $this->createView()->renderPartial($view, $params); + return Yii::$app->getView()->render($view, $params, $this); } /** - * @return View + * Renders a view file. + * @param string $file the view file to be rendered. This can be either a file path or a path alias. + * @param array $params the parameters (name-value pairs) that should be made available in the view. + * @return string the rendering result. + * @throws InvalidParamException if the view file does not exist. */ - public function createView() + public function renderFile($file, $params = array()) { - return new View($this); + return Yii::$app->getView()->renderFile($file, $params, $this); } /** diff --git a/framework/caching/ApcCache.php b/framework/caching/ApcCache.php index b4df296..dd954cc 100644 --- a/framework/caching/ApcCache.php +++ b/framework/caching/ApcCache.php @@ -1,9 +1,7 @@ <?php /** - * ApcCache class file - * * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ diff --git a/framework/caching/Cache.php b/framework/caching/Cache.php index a35785c..70cf8cb 100644 --- a/framework/caching/Cache.php +++ b/framework/caching/Cache.php @@ -1,15 +1,14 @@ <?php /** - * Cache class file. - * * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ namespace yii\caching; use yii\base\Component; +use yii\helpers\StringHelper; /** * Cache is the base class for cache classes supporting different cache storage implementation. @@ -72,13 +71,13 @@ abstract class Cache extends Component implements \ArrayAccess /** - * Builds a normalized cache key from one or multiple parameters. + * Builds a normalized cache key from a given key. * * The generated key contains letters and digits only, and its length is no more than 32. * - * If only one parameter is given and it is already a normalized key, then - * it will be returned back without change. Otherwise, a normalized key - * is generated by serializing all given parameters and applying MD5 hashing. + * If the given key is a string containing alphanumeric characters only and no more than 32 characters, + * then the key will be returned back without change. Otherwise, a normalized key + * is generated by serializing the given key and applying MD5 hashing. * * The following example builds a cache key using three parameters: * @@ -86,16 +85,15 @@ abstract class Cache extends Component implements \ArrayAccess * $key = $cache->buildKey($className, $method, $id); * ~~~ * - * @param string $key the first parameter + * @param array|string $key the key to be normalized * @return string the generated cache key */ public function buildKey($key) { - if (func_num_args() === 1 && ctype_alnum($key) && strlen($key) <= 32) { - return (string)$key; + if (is_string($key)) { + return ctype_alnum($key) && StringHelper::strlen($key) <= 32 ? $key : md5($key); } else { - $params = func_get_args(); - return md5(serialize($params)); + return md5(json_encode($key)); } } diff --git a/framework/caching/ChainedDependency.php b/framework/caching/ChainedDependency.php index 570715d..9c4e547 100644 --- a/framework/caching/ChainedDependency.php +++ b/framework/caching/ChainedDependency.php @@ -1,9 +1,7 @@ <?php /** - * ChainedDependency class file. - * * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ diff --git a/framework/caching/DbCache.php b/framework/caching/DbCache.php index 4b84bfd..3952852 100644 --- a/framework/caching/DbCache.php +++ b/framework/caching/DbCache.php @@ -1,14 +1,13 @@ <?php /** - * DbCache class file - * * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ namespace yii\caching; +use Yii; use yii\base\InvalidConfigException; use yii\db\Connection; use yii\db\Query; @@ -16,30 +15,20 @@ use yii\db\Query; /** * DbCache implements a cache application component by storing cached data in a database. * - * DbCache stores cache data in a DB table whose name is specified via [[cacheTableName]]. - * For MySQL database, the table should be created beforehand as follows : - * - * ~~~ - * CREATE TABLE tbl_cache ( - * id char(128) NOT NULL, - * expire int(11) DEFAULT NULL, - * data LONGBLOB, - * PRIMARY KEY (id), - * KEY expire (expire) - * ); - * ~~~ - * - * You should replace `LONGBLOB` as follows if you are using a different DBMS: - * - * - PostgreSQL: `BYTEA` - * - SQLite, SQL server, Oracle: `BLOB` - * - * DbCache connects to the database via the DB connection specified in [[connectionID]] - * which must refer to a valid DB application component. + * By default, DbCache stores session data in a DB table named 'tbl_cache'. This table + * must be pre-created. The table name can be changed by setting [[cacheTable]]. * * Please refer to [[Cache]] for common cache operations that are supported by DbCache. * - * @property Connection $db The DB connection instance. + * The following example shows how you can configure the application to use DbCache: + * + * ~~~ + * 'cache' => array( + * 'class' => 'yii\caching\DbCache', + * // 'db' => 'mydb', + * // 'cacheTable' => 'my_cache', + * ) + * ~~~ * * @author Qiang Xue <qiang.xue@gmail.com> * @since 2.0 @@ -47,50 +36,56 @@ use yii\db\Query; class DbCache extends Cache { /** - * @var string the ID of the [[Connection|DB connection]] application component. Defaults to 'db'. + * @var Connection|string the DB connection object or the application component ID of the DB connection. + * After the DbCache object is created, if you want to change this property, you should only assign it + * with a DB connection object. */ - public $connectionID = 'db'; + public $db = 'db'; /** - * @var string name of the DB table to store cache content. Defaults to 'tbl_cache'. - * The table must be created before using this cache component. + * @var string name of the DB table to store cache content. + * The table should be pre-created as follows: + * + * ~~~ + * CREATE TABLE tbl_cache ( + * id char(128) NOT NULL PRIMARY KEY, + * expire int(11), + * data BLOB + * ); + * ~~~ + * + * where 'BLOB' refers to the BLOB-type of your preferred DBMS. Below are the BLOB type + * that can be used for some popular DBMS: + * + * - MySQL: LONGBLOB + * - PostgreSQL: BYTEA + * - MSSQL: BLOB + * + * When using DbCache in a production server, we recommend you create a DB index for the 'expire' + * column in the cache table to improve the performance. */ - public $cacheTableName = 'tbl_cache'; + public $cacheTable = 'tbl_cache'; /** * @var integer the probability (parts per million) that garbage collection (GC) should be performed - * when storing a piece of data in the cache. Defaults to 10, meaning 0.001% chance. + * when storing a piece of data in the cache. Defaults to 100, meaning 0.01% chance. * This number should be between 0 and 1000000. A value 0 meaning no GC will be performed at all. **/ public $gcProbability = 100; - /** - * @var Connection the DB connection instance - */ - private $_db; - /** - * Returns the DB connection instance used for caching purpose. - * @return Connection the DB connection instance - * @throws InvalidConfigException if [[connectionID]] does not point to a valid application component. - */ - public function getDb() - { - if ($this->_db === null) { - $db = \Yii::$application->getComponent($this->connectionID); - if ($db instanceof Connection) { - $this->_db = $db; - } else { - throw new InvalidConfigException("DbCache::connectionID must refer to the ID of a DB application component."); - } - } - return $this->_db; - } /** - * Sets the DB connection used by the cache component. - * @param Connection $value the DB connection instance + * Initializes the DbCache component. + * This method will initialize the [[db]] property to make sure it refers to a valid DB connection. + * @throws InvalidConfigException if [[db]] is invalid. */ - public function setDb($value) + public function init() { - $this->_db = $value; + parent::init(); + if (is_string($this->db)) { + $this->db = Yii::$app->getComponent($this->db); + } + if (!$this->db instanceof Connection) { + throw new InvalidConfigException("DbCache::db must be either a DB connection instance or the application component ID of a DB connection."); + } } /** @@ -103,17 +98,16 @@ class DbCache extends Cache { $query = new Query; $query->select(array('data')) - ->from($this->cacheTableName) - ->where('id = :id AND (expire = 0 OR expire > :time)', array(':id' => $key, ':time' => time())); - $db = $this->getDb(); - if ($db->enableQueryCache) { + ->from($this->cacheTable) + ->where('id = :id AND (expire = 0 OR expire >' . time() . ')', array(':id' => $key)); + if ($this->db->enableQueryCache) { // temporarily disable and re-enable query caching - $db->enableQueryCache = false; - $result = $query->createCommand($db)->queryScalar(); - $db->enableQueryCache = true; + $this->db->enableQueryCache = false; + $result = $query->createCommand($this->db)->queryScalar(); + $this->db->enableQueryCache = true; return $result; } else { - return $query->createCommand($db)->queryScalar(); + return $query->createCommand($this->db)->queryScalar(); } } @@ -129,17 +123,16 @@ class DbCache extends Cache } $query = new Query; $query->select(array('id', 'data')) - ->from($this->cacheTableName) + ->from($this->cacheTable) ->where(array('id' => $keys)) - ->andWhere("expire = 0 OR expire > " . time() . ")"); + ->andWhere('(expire = 0 OR expire > ' . time() . ')'); - $db = $this->getDb(); - if ($db->enableQueryCache) { - $db->enableQueryCache = false; - $rows = $query->createCommand($db)->queryAll(); - $db->enableQueryCache = true; + if ($this->db->enableQueryCache) { + $this->db->enableQueryCache = false; + $rows = $query->createCommand($this->db)->queryAll(); + $this->db->enableQueryCache = true; } else { - $rows = $query->createCommand($db)->queryAll(); + $rows = $query->createCommand($this->db)->queryAll(); } $results = array(); @@ -163,13 +156,13 @@ class DbCache extends Cache */ protected function setValue($key, $value, $expire) { - $command = $this->getDb()->createCommand(); - $command->update($this->cacheTableName, array( - 'expire' => $expire > 0 ? $expire + time() : 0, - 'data' => array($value, \PDO::PARAM_LOB), - ), array( - 'id' => $key, - ));; + $command = $this->db->createCommand() + ->update($this->cacheTable, array( + 'expire' => $expire > 0 ? $expire + time() : 0, + 'data' => array($value, \PDO::PARAM_LOB), + ), array( + 'id' => $key, + )); if ($command->execute()) { $this->gc(); @@ -198,14 +191,13 @@ class DbCache extends Cache $expire = 0; } - $command = $this->getDb()->createCommand(); - $command->insert($this->cacheTableName, array( - 'id' => $key, - 'expire' => $expire, - 'data' => array($value, \PDO::PARAM_LOB), - )); try { - $command->execute(); + $this->db->createCommand() + ->insert($this->cacheTable, array( + 'id' => $key, + 'expire' => $expire, + 'data' => array($value, \PDO::PARAM_LOB), + ))->execute(); return true; } catch (\Exception $e) { return false; @@ -220,8 +212,9 @@ class DbCache extends Cache */ protected function deleteValue($key) { - $command = $this->getDb()->createCommand(); - $command->delete($this->cacheTableName, array('id' => $key))->execute(); + $this->db->createCommand() + ->delete($this->cacheTable, array('id' => $key)) + ->execute(); return true; } @@ -233,8 +226,9 @@ class DbCache extends Cache public function gc($force = false) { if ($force || mt_rand(0, 1000000) < $this->gcProbability) { - $command = $this->getDb()->createCommand(); - $command->delete($this->cacheTableName, 'expire > 0 AND expire < ' . time())->execute(); + $this->db->createCommand() + ->delete($this->cacheTable, 'expire > 0 AND expire < ' . time()) + ->execute(); } } @@ -245,8 +239,9 @@ class DbCache extends Cache */ protected function flushValues() { - $command = $this->getDb()->createCommand(); - $command->delete($this->cacheTableName)->execute(); + $this->db->createCommand() + ->delete($this->cacheTable) + ->execute(); return true; } } diff --git a/framework/caching/DbDependency.php b/framework/caching/DbDependency.php index 7ffdb4e..cbe0ae1 100644 --- a/framework/caching/DbDependency.php +++ b/framework/caching/DbDependency.php @@ -1,23 +1,21 @@ <?php /** - * DbDependency class file. - * * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ namespace yii\caching; +use Yii; use yii\base\InvalidConfigException; use yii\db\Connection; -use yii\db\Query; /** * DbDependency represents a dependency based on the query result of a SQL statement. * * If the query result changes, the dependency is considered as changed. - * The query is specified via the [[query]] property. + * The query is specified via the [[sql]] property. * * @author Qiang Xue <qiang.xue@gmail.com> * @since 2.0 @@ -25,88 +23,52 @@ use yii\db\Query; class DbDependency extends Dependency { /** - * @var string the ID of the [[Connection|DB connection]] application component. Defaults to 'db'. + * @var string the application component ID of the DB connection. */ - public $connectionID = 'db'; + public $db = 'db'; /** - * @var Query the SQL query whose result is used to determine if the dependency has been changed. + * @var string the SQL query whose result is used to determine if the dependency has been changed. * Only the first row of the query result will be used. */ - public $query; + public $sql; /** - * @var Connection the DB connection instance + * @var array the parameters (name=>value) to be bound to the SQL statement specified by [[sql]]. */ - private $_db; + public $params; /** * Constructor. - * @param Query $query the SQL query whose result is used to determine if the dependency has been changed. + * @param string $sql the SQL query whose result is used to determine if the dependency has been changed. + * @param array $params the parameters (name=>value) to be bound to the SQL statement specified by [[sql]]. * @param array $config name-value pairs that will be used to initialize the object properties */ - public function __construct($query = null, $config = array()) + public function __construct($sql, $params = array(), $config = array()) { - $this->query = $query; + $this->sql = $sql; + $this->params = $params; parent::__construct($config); } /** - * PHP sleep magic method. - * This method ensures that the database instance is set null because it contains resource handles. - * @return array - */ - public function __sleep() - { - $this->_db = null; - return array_keys((array)$this); - } - - /** * Generates the data needed to determine if dependency has been changed. * This method returns the value of the global state. * @return mixed the data needed to determine if dependency has been changed. */ protected function generateDependencyData() { - $db = $this->getDb(); - /** - * @var \yii\db\Command $command - */ - $command = $this->query->createCommand($db); + $db = Yii::$app->getComponent($this->db); + if (!$db instanceof Connection) { + throw new InvalidConfigException("DbDependency::db must be the application component ID of a DB connection."); + } + if ($db->enableQueryCache) { // temporarily disable and re-enable query caching $db->enableQueryCache = false; - $result = $command->queryRow(); + $result = $db->createCommand($this->sql, $this->params)->queryRow(); $db->enableQueryCache = true; } else { - $result = $command->queryRow(); + $result = $db->createCommand($this->sql, $this->params)->queryRow(); } return $result; } - - /** - * Returns the DB connection instance used for caching purpose. - * @return Connection the DB connection instance - * @throws InvalidConfigException if [[connectionID]] does not point to a valid application component. - */ - public function getDb() - { - if ($this->_db === null) { - $db = \Yii::$application->getComponent($this->connectionID); - if ($db instanceof Connection) { - $this->_db = $db; - } else { - throw new InvalidConfigException("DbCache::connectionID must refer to the ID of a DB application component."); - } - } - return $this->_db; - } - - /** - * Sets the DB connection used by the cache component. - * @param Connection $value the DB connection instance - */ - public function setDb($value) - { - $this->_db = $value; - } } diff --git a/framework/caching/Dependency.php b/framework/caching/Dependency.php index 2e66145..feb8c07 100644 --- a/framework/caching/Dependency.php +++ b/framework/caching/Dependency.php @@ -1,9 +1,7 @@ <?php /** - * Dependency class file. - * * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ diff --git a/framework/caching/DummyCache.php b/framework/caching/DummyCache.php index f6e8a44..359fa7c 100644 --- a/framework/caching/DummyCache.php +++ b/framework/caching/DummyCache.php @@ -1,9 +1,7 @@ <?php /** - * DummyCache class file. - * * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ @@ -13,7 +11,7 @@ namespace yii\caching; * DummyCache is a placeholder cache component. * * DummyCache does not cache anything. It is provided so that one can always configure - * a 'cache' application component and save the check of existence of `\Yii::$application->cache`. + * a 'cache' application component and save the check of existence of `\Yii::$app->cache`. * By replacing DummyCache with some other cache component, one can quickly switch from * non-caching mode to caching mode. * diff --git a/framework/caching/ExpressionDependency.php b/framework/caching/ExpressionDependency.php index 7ad7543..e13c962 100644 --- a/framework/caching/ExpressionDependency.php +++ b/framework/caching/ExpressionDependency.php @@ -1,9 +1,7 @@ <?php /** - * ExpressionDependency class file. - * * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ diff --git a/framework/caching/FileCache.php b/framework/caching/FileCache.php index f97861f..e565cad 100644 --- a/framework/caching/FileCache.php +++ b/framework/caching/FileCache.php @@ -1,9 +1,7 @@ <?php /** - * FileCache class file - * * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ @@ -28,7 +26,7 @@ class FileCache extends Cache /** * @var string the directory to store cache files. You may use path alias here. */ - public $cachePath = '@application/runtime/cache'; + public $cachePath = '@app/runtime/cache'; /** * @var string cache file suffix. Defaults to '.bin'. */ @@ -54,9 +52,6 @@ class FileCache extends Cache { parent::init(); $this->cachePath = \Yii::getAlias($this->cachePath); - if ($this->cachePath === false) { - throw new InvalidConfigException('FileCache.cachePath must be a valid path alias.'); - } if (!is_dir($this->cachePath)) { mkdir($this->cachePath, 0777, true); } diff --git a/framework/caching/FileDependency.php b/framework/caching/FileDependency.php index 89b356c..3797dde 100644 --- a/framework/caching/FileDependency.php +++ b/framework/caching/FileDependency.php @@ -1,9 +1,7 @@ <?php /** - * FileDependency class file. - * * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ diff --git a/framework/caching/MemCache.php b/framework/caching/MemCache.php index 288c3ee..df07b8e 100644 --- a/framework/caching/MemCache.php +++ b/framework/caching/MemCache.php @@ -1,9 +1,7 @@ <?php /** - * MemCache class file - * * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ diff --git a/framework/caching/MemCacheServer.php b/framework/caching/MemCacheServer.php index 13e929e..105137e 100644 --- a/framework/caching/MemCacheServer.php +++ b/framework/caching/MemCacheServer.php @@ -1,9 +1,7 @@ <?php /** - * MemCacheServer class file - * * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ diff --git a/framework/caching/WinCache.php b/framework/caching/WinCache.php index 8bb6569..ee6b4a9 100644 --- a/framework/caching/WinCache.php +++ b/framework/caching/WinCache.php @@ -1,9 +1,7 @@ <?php /** - * WinCache class file - * * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ diff --git a/framework/caching/XCache.php b/framework/caching/XCache.php index af5501c..2108c4f 100644 --- a/framework/caching/XCache.php +++ b/framework/caching/XCache.php @@ -1,9 +1,7 @@ <?php /** - * XCache class file - * * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ diff --git a/framework/caching/ZendDataCache.php b/framework/caching/ZendDataCache.php index 8716a36..669733d 100644 --- a/framework/caching/ZendDataCache.php +++ b/framework/caching/ZendDataCache.php @@ -1,9 +1,7 @@ <?php /** - * ZendDataCache class file - * * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ diff --git a/framework/console/Application.php b/framework/console/Application.php index 237be05..574495b 100644 --- a/framework/console/Application.php +++ b/framework/console/Application.php @@ -3,13 +3,12 @@ * Console Application class file. * * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ namespace yii\console; -use yii\base\Exception; use yii\base\InvalidRouteException; /** @@ -85,16 +84,17 @@ class Application extends \yii\base\Application * Processes the request. * The request is represented in terms of a controller route and action parameters. * @return integer the exit status of the controller action (0 means normal, non-zero values mean abnormal) + * @throws Exception if the script is not running from the command line */ public function processRequest() { /** @var $request Request */ $request = $this->getRequest(); if ($request->getIsConsoleRequest()) { - return $this->runAction($request->route, $request->params); + list ($route, $params) = $request->resolve(); + return $this->runAction($route, $params); } else { - echo "Error: this script must be run from the command line."; - return 1; + throw new Exception(\Yii::t('yii|This script must be run from the command line.')); } } @@ -106,14 +106,14 @@ class Application extends \yii\base\Application * @param string $route the route that specifies the action. * @param array $params the parameters to be passed to the action * @return integer the status code returned by the action execution. 0 means normal, and other values mean abnormal. + * @throws Exception if the route is invalid */ public function runAction($route, $params = array()) { try { return parent::runAction($route, $params); } catch (InvalidRouteException $e) { - echo "Error: unknown command \"$route\".\n"; - return 1; + throw new Exception(\Yii::t('yii|Unknown command "{command}".', array('{command}' => $route))); } } @@ -127,8 +127,8 @@ class Application extends \yii\base\Application 'message' => 'yii\console\controllers\MessageController', 'help' => 'yii\console\controllers\HelpController', 'migrate' => 'yii\console\controllers\MigrateController', - 'shell' => 'yii\console\controllers\ShellController', - 'create' => 'yii\console\controllers\CreateController', + 'app' => 'yii\console\controllers\AppController', + 'cache' => 'yii\console\controllers\CacheController', ); } @@ -148,9 +148,4 @@ class Application extends \yii\base\Application ), )); } - - public function usageError($message) - { - - } } diff --git a/framework/console/Controller.php b/framework/console/Controller.php index 16968f2..9924822 100644 --- a/framework/console/Controller.php +++ b/framework/console/Controller.php @@ -1,9 +1,7 @@ <?php /** - * Controller class file. - * * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ @@ -11,7 +9,7 @@ namespace yii\console; use Yii; use yii\base\Action; -use yii\base\InvalidRequestException; +use yii\base\InlineAction; use yii\base\InvalidRouteException; /** @@ -49,14 +47,11 @@ class Controller extends \yii\base\Controller public function runAction($id, $params = array()) { if ($params !== array()) { - $class = new \ReflectionClass($this); + $options = $this->globalOptions(); foreach ($params as $name => $value) { - if ($class->hasProperty($name)) { - $property = $class->getProperty($name); - if ($property->isPublic() && !$property->isStatic() && $property->getDeclaringClass()->getName() === get_class($this)) { - $this->$name = $value; - unset($params[$name]); - } + if (in_array($name, $options, true)) { + $this->$name = $value; + unset($params[$name]); } } } @@ -64,25 +59,60 @@ class Controller extends \yii\base\Controller } /** - * Validates the parameter being bound to actions. - * This method is invoked when parameters are being bound to the currently requested action. - * Child classes may override this method to throw exceptions when there are missing and/or unknown parameters. - * @param Action $action the currently requested action - * @param array $missingParams the names of the missing parameters - * @param array $unknownParams the unknown parameters (name=>value) - * @throws InvalidRequestException if there are missing or unknown parameters + * Binds the parameters to the action. + * This method is invoked by [[Action]] when it begins to run with the given parameters. + * This method will first bind the parameters with the [[globalOptions()|global options]] + * available to the action. It then validates the given arguments. + * @param Action $action the action to be bound with parameters + * @param array $params the parameters to be bound to the action + * @return array the valid parameters that the action can run with. + * @throws Exception if there are unknown options or missing arguments */ - public function validateActionParams($action, $missingParams, $unknownParams) + public function bindActionParams($action, $params) { - if (!empty($missingParams)) { - throw new InvalidRequestException(Yii::t('yii', 'Missing required options: {params}', array( - '{params}' => implode(', ', $missingParams), + if ($params !== array()) { + $options = $this->globalOptions(); + foreach ($params as $name => $value) { + if (in_array($name, $options, true)) { + $this->$name = $value; + unset($params[$name]); + } + } + } + + $args = isset($params[Request::ANONYMOUS_PARAMS]) ? $params[Request::ANONYMOUS_PARAMS] : array(); + unset($params[Request::ANONYMOUS_PARAMS]); + if ($params !== array()) { + throw new Exception(Yii::t('yii|Unknown options: {params}', array( + '{params}' => implode(', ', array_keys($params)), ))); - } elseif (!empty($unknownParams)) { - throw new InvalidRequestException(Yii::t('yii', 'Unknown options: {params}', array( - '{params}' => implode(', ', $unknownParams), + } + + if ($action instanceof InlineAction) { + $method = new \ReflectionMethod($this, $action->actionMethod); + } else { + $method = new \ReflectionMethod($action, 'run'); + } + + $missing = array(); + foreach ($method->getParameters() as $i => $param) { + $name = $param->getName(); + if (!isset($args[$i])) { + if ($param->isDefaultValueAvailable()) { + $args[$i] = $param->getDefaultValue(); + } else { + $missing[] = $name; + } + } + } + + if ($missing !== array()) { + throw new Exception(Yii::t('yii|Missing required arguments: {params}', array( + '{params}' => implode(', ', $missing), ))); } + + return $args; } /** @@ -103,12 +133,17 @@ class Controller extends \yii\base\Controller } } - public function usageError($message) - { - echo "\nError: $message\n"; - Yii::$application->end(1); - } - + /** + * Returns the names of the global options for this command. + * A global option requires the existence of a public member variable whose + * name is the option name. + * Child classes may override this method to specify possible global options. + * + * Note that the values setting via global options are not available + * until [[beforeAction()]] is being called. + * + * @return array the names of the global options for this command. + */ public function globalOptions() { return array(); diff --git a/framework/console/Exception.php b/framework/console/Exception.php new file mode 100644 index 0000000..cb10c19 --- /dev/null +++ b/framework/console/Exception.php @@ -0,0 +1,28 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\console; + +use yii\base\UserException; + +/** + * Exception represents an exception caused by incorrect usage of a console command. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class Exception extends UserException +{ + /** + * @return string the user-friendly name of this exception + */ + public function getName() + { + return \Yii::t('yii|Error'); + } +} + diff --git a/framework/console/Request.php b/framework/console/Request.php index dbf80ba..ed477e9 100644 --- a/framework/console/Request.php +++ b/framework/console/Request.php @@ -1,9 +1,7 @@ <?php /** - * Request class file. - * * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ @@ -15,49 +13,39 @@ namespace yii\console; */ class Request extends \yii\base\Request { - /** - * @var string the controller route specified by this request. If this is an empty string, - * it means the [[Application::defaultRoute|default route]] will be used. - * Note that the value of this property may not be a correct route. The console application - * will determine it is valid or not when it attempts to execute with this route. - */ - public $route; - /** - * @var array - */ - public $params; - - public function init() - { - parent::init(); - $this->resolveRequest(); - } + const ANONYMOUS_PARAMS = '-args'; public function getRawParams() { return isset($_SERVER['argv']) ? $_SERVER['argv'] : array(); } - protected function resolveRequest() + /** + * Resolves the current request into a route and the associated parameters. + * @return array the first element is the route, and the second is the associated parameters. + */ + public function resolve() { $rawParams = $this->getRawParams(); array_shift($rawParams); // the 1st argument is the yiic script name if (isset($rawParams[0])) { - $this->route = $rawParams[0]; + $route = $rawParams[0]; array_shift($rawParams); } else { - $this->route = ''; + $route = ''; } - $this->params = array(); + $params = array(self::ANONYMOUS_PARAMS => array()); foreach ($rawParams as $param) { if (preg_match('/^--(\w+)(=(.*))?$/', $param, $matches)) { $name = $matches[1]; - $this->params[$name] = isset($matches[3]) ? $matches[3] : true; + $params[$name] = isset($matches[3]) ? $matches[3] : true; } else { - $this->params['args'][] = $param; + $params[self::ANONYMOUS_PARAMS][] = $param; } } + + return array($route, $params); } } diff --git a/framework/console/controllers/CreateController.php b/framework/console/controllers/AppController.php similarity index 90% rename from framework/console/controllers/CreateController.php rename to framework/console/controllers/AppController.php index 7bd7fd0..93ef5f5 100644 --- a/framework/console/controllers/CreateController.php +++ b/framework/console/controllers/AppController.php @@ -1,16 +1,14 @@ <?php /** - * CreateController class file. - * * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ namespace yii\console\controllers; use yii\console\Controller; -use yii\util\FileHelper; +use yii\helpers\FileHelper; use yii\base\Exception; /** @@ -20,14 +18,14 @@ use yii\base\Exception; * @author Alexander Makarov <sam@rmcreative.ru> * @since 2.0 */ -class CreateController extends Controller +class AppController extends Controller { private $_rootPath; private $_config; /** * @var string custom template path. If specified, templates will be - * searched there additionally to `framework/console/create`. + * searched there additionally to `framework/console/webapp`. */ public $templatesPath; @@ -46,6 +44,16 @@ class CreateController extends Controller } } + public function globalOptions() + { + return array('templatesPath', 'type'); + } + + public function actionIndex() + { + $this->forward('help/index', array('-args' => array('app/create'))); + } + /** * Generates Yii application at the path specified via appPath parameter. * @@ -56,7 +64,7 @@ class CreateController extends Controller * @throws \yii\base\Exception if path specified is not valid * @return integer the exit status */ - public function actionIndex($path) + public function actionCreate($path) { $path = strtr($path, '/\\', DIRECTORY_SEPARATOR); if(strpos($path, DIRECTORY_SEPARATOR) === false) { @@ -127,7 +135,7 @@ class CreateController extends Controller */ protected function getDefaultTemplatesPath() { - return realpath(__DIR__.'/../create'); + return realpath(__DIR__.'/../webapp'); } /** diff --git a/framework/console/controllers/CacheController.php b/framework/console/controllers/CacheController.php new file mode 100644 index 0000000..6765f9b --- /dev/null +++ b/framework/console/controllers/CacheController.php @@ -0,0 +1,47 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\console\controllers; + +use yii\console\Controller; +use yii\console\Exception; +use yii\caching\Cache; + +/** + * This command allows you to flush cache. + * + * @author Alexander Makarov <sam@rmcreative.ru> + * @since 2.0 + */ +class CacheController extends Controller +{ + public function actionIndex() + { + $this->forward('help/index', array('-args' => array('cache/flush'))); + } + + /** + * Flushes cache. + * @param string $component Name of the cache application component to use. + * + * @throws \yii\console\Exception + */ + public function actionFlush($component = 'cache') + { + /** @var $cache Cache */ + $cache = \Yii::$app->getComponent($component); + if(!$cache || !$cache instanceof Cache) { + throw new Exception('Application component "'.$component.'" is not defined or not a cache.'); + } + + if(!$cache->flush()) { + throw new Exception('Unable to flush cache.'); + } + + echo "\nDone.\n"; + } +} diff --git a/framework/console/controllers/HelpController.php b/framework/console/controllers/HelpController.php index 6e4b397..ea7e3d5 100644 --- a/framework/console/controllers/HelpController.php +++ b/framework/console/controllers/HelpController.php @@ -1,18 +1,19 @@ <?php /** - * HelpController class file. - * * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ namespace yii\console\controllers; +use Yii; use yii\base\Application; +use yii\console\Exception; use yii\base\InlineAction; use yii\console\Controller; -use yii\util\StringHelper; +use yii\console\Request; +use yii\helpers\StringHelper; /** * This command provides help information about console commands. @@ -44,30 +45,32 @@ class HelpController extends Controller * yiic help message # display help info about "message" * ~~~ * - * @param array $args additional anonymous command line arguments. - * You may provide a command name to display its detailed information. + * @param string $command The name of the command to show help about. + * If not provided, all available commands will be displayed. * @return integer the exit status + * @throws Exception if the command for help is unknown */ - public function actionIndex($args = array()) + public function actionIndex($command = null) { - if (empty($args)) { - $status = $this->getHelp(); - } else { - $result = \Yii::$application->createController($args[0]); + if ($command !== null) { + $result = Yii::$app->createController($command); if ($result === false) { - echo "Error: no help for unknown command \"{$args[0]}\".\n"; - return 1; + throw new Exception(Yii::t('yii|No help for unknown command "{command}".', array( + '{command}' => $command, + ))); } list($controller, $actionID) = $result; - if ($actionID === '') { - $status = $this->getControllerHelp($controller); + $actions = $this->getActions($controller); + if ($actionID !== '' || count($actions) === 1 && $actions[0] === $controller->defaultAction) { + $this->getActionHelp($controller, $actionID); } else { - $status = $this->getActionHelp($controller, $actionID); + $this->getControllerHelp($controller); } + } else { + $this->getHelp(); } - return $status; } /** @@ -76,7 +79,7 @@ class HelpController extends Controller */ public function getCommands() { - $commands = $this->getModuleCommands(\Yii::$application); + $commands = $this->getModuleCommands(Yii::$app); sort($commands); return array_unique($commands); } @@ -91,7 +94,6 @@ class HelpController extends Controller $actions = array_keys($controller->actions()); $class = new \ReflectionClass($controller); foreach ($class->getMethods() as $method) { - /** @var $method \ReflectionMethod */ $name = $method->getName(); if ($method->isPublic() && !$method->isStatic() && strpos($name, 'action') === 0 && $name !== 'actions') { $actions[] = StringHelper::camel2id(substr($name, 6)); @@ -136,29 +138,25 @@ class HelpController extends Controller /** * Displays all available commands. - * @return integer the exit status */ protected function getHelp() { $commands = $this->getCommands(); if ($commands !== array()) { - echo "\nUsage: yiic <command-name> [...options...]\n\n"; echo "The following commands are available:\n\n"; foreach ($commands as $command) { - echo " * $command\n"; + echo "* $command\n"; } echo "\nTo see the help of each command, enter:\n"; - echo "\n yiic help <command-name>\n"; + echo "\n yiic help <command-name>\n\n"; } else { echo "\nNo commands are found.\n"; } - return 0; } /** * Displays the overall information of the command. * @param Controller $controller the controller instance - * @return integer the exit status */ protected function getControllerHelp($controller) { @@ -169,181 +167,255 @@ class HelpController extends Controller } if ($comment !== '') { - echo "\n" . $comment . "\n"; - } - - $options = $this->getGlobalOptions($class, $controller); - if ($options !== array()) { - echo "\nGLOBAL OPTIONS"; - echo "\n--------------\n\n"; - foreach ($options as $name => $description) { - echo " --$name"; - if ($description != '') { - echo ": $description\n"; - } - echo "\n"; - } + echo "\nDESCRIPTION\n"; + echo "\n" . $comment . "\n\n"; } $actions = $this->getActions($controller); if ($actions !== array()) { - echo "\nSUB-COMMANDS"; - echo "\n------------\n\n"; + echo "\nSUB-COMMANDS\n\n"; $prefix = $controller->getUniqueId(); foreach ($actions as $action) { - if ($controller->defaultAction === $action) { - echo " * $prefix (default)\n"; + if ($action === $controller->defaultAction) { + echo "* $prefix/$action (default)"; } else { - echo " * $prefix/$action\n"; + echo "* $prefix/$action"; } + $summary = $this->getActionSummary($controller, $action); + if ($summary !== '') { + echo ': ' . $summary; + } + echo "\n"; } - echo "\n"; + echo "\n\nTo see the detailed information about individual sub-commands, enter:\n"; + echo "\n yiic help <sub-command>\n\n"; } + } - return 0; + /** + * Returns the short summary of the action. + * @param Controller $controller the controller instance + * @param string $actionID action ID + * @return string the summary about the action + */ + protected function getActionSummary($controller, $actionID) + { + $action = $controller->createAction($actionID); + if ($action === null) { + return ''; + } + if ($action instanceof InlineAction) { + $reflection = new \ReflectionMethod($controller, $action->actionMethod); + } else { + $reflection = new \ReflectionClass($action); + } + $tags = $this->parseComment($reflection->getDocComment()); + if ($tags['description'] !== '') { + $limit = 73 - strlen($action->getUniqueId()); + if ($actionID === $controller->defaultAction) { + $limit -= 10; + } + if ($limit < 0) { + $limit = 50; + } + $description = $tags['description']; + if (($pos = strpos($tags['description'], "\n")) !== false) { + $description = substr($description, 0, $pos); + } + $text = substr($description, 0, $limit); + return strlen($description) > $limit ? $text . '...' : $text; + } else { + return ''; + } } /** * Displays the detailed information of a command action. * @param Controller $controller the controller instance * @param string $actionID action ID - * @return integer the exit status + * @throws Exception if the action does not exist */ protected function getActionHelp($controller, $actionID) { $action = $controller->createAction($actionID); if ($action === null) { - echo 'Error: no help for unknown sub-command "' . $controller->getUniqueId() . "/$actionID\".\n"; - return 1; + throw new Exception(Yii::t('yii|No help for unknown sub-command "{command}".', array( + '{command}' => rtrim($controller->getUniqueId() . '/' . $actionID, '/'), + ))); } if ($action instanceof InlineAction) { - $method = new \ReflectionMethod($controller, 'action' . $action->id); + $method = new \ReflectionMethod($controller, $action->actionMethod); } else { $method = new \ReflectionMethod($action, 'run'); } - $comment = strtr(trim(preg_replace('/^\s*\**( |\t)?/m', '', trim($method->getDocComment(), '/'))), "\r", ''); - if (preg_match('/^\s*@\w+/m', $comment, $matches, PREG_OFFSET_CAPTURE)) { - $meta = substr($comment, $matches[0][1]); - $comment = trim(substr($comment, 0, $matches[0][1])); + + $tags = $this->parseComment($method->getDocComment()); + $options = $this->getOptionHelps($controller); + + if ($tags['description'] !== '') { + echo "\nDESCRIPTION"; + echo "\n\n" . $tags['description'] . "\n\n"; + } + + echo "\nUSAGE\n\n"; + if ($action->id === $controller->defaultAction) { + echo 'yiic ' . $controller->getUniqueId(); } else { - $meta = ''; + echo "yiic " . $action->getUniqueId(); + } + list ($required, $optional) = $this->getArgHelps($method, isset($tags['param']) ? $tags['param'] : array()); + if (!empty($required)) { + echo ' <' . implode('> <', array_keys($required)) . '>'; + } + if (!empty($optional)) { + echo ' [' . implode('] [', array_keys($optional)) . ']'; } + if (!empty($options)) { + echo ' [...options...]'; + } + echo "\n\n"; - if ($comment !== '') { - echo "\n" . $comment . "\n"; + if (!empty($required) || !empty($optional)) { + echo implode("\n\n", array_merge($required, $optional)) . "\n\n"; } - $options = $this->getOptions($method, $meta); + $options = $this->getOptionHelps($controller); if ($options !== array()) { - echo "\nOPTIONS"; - echo "\n-------\n\n"; - foreach ($options as $name => $description) { - echo " --$name"; - if ($description != '') { - echo ": $description\n"; - } - } - echo "\n"; + echo "\nOPTIONS\n\n"; + echo implode("\n\n", $options) . "\n\n"; } - - return 0; } /** + * Returns the help information about arguments. * @param \ReflectionMethod $method - * @param string $meta - * @return array + * @param string $tags the parsed comment block related with arguments + * @return array the required and optional argument help information */ - protected function getOptions($method, $meta) + protected function getArgHelps($method, $tags) { + if (is_string($tags)) { + $tags = array($tags); + } $params = $method->getParameters(); - $tags = preg_split('/^\s*@/m', $meta, -1, PREG_SPLIT_NO_EMPTY); - $options = array(); - $count = 0; - foreach ($tags as $tag) { - $parts = preg_split('/\s+/', trim($tag), 2); - if ($parts[0] === 'param' && isset($params[$count])) { - $param = $params[$count]; - $comment = isset($parts[1]) ? $parts[1] : ''; - if (preg_match('/^([^\s]+)\s+(\$\w+\s+)?(.*)/s', $comment, $matches)) { - $type = $matches[1]; - $doc = $matches[3]; - } else { - $type = $comment; - $doc = ''; - } - $comment = $type === '' ? '' : ($type . ', '); - if ($param->isDefaultValueAvailable()) { - $value = $param->getDefaultValue(); - if (!is_array($value)) { - $comment .= 'optional (defaults to ' . var_export($value, true) . ').'; - } else { - $comment .= 'optional.'; - } - } else { - $comment .= 'required.'; - } - if (trim($doc) !== '') { - $comment .= "\n" . preg_replace("/^/m", " ", $doc); - } - $options[$param->getName()] = $comment; - $count++; + $optional = $required = array(); + foreach ($params as $i => $param) { + $name = $param->getName(); + $tag = isset($tags[$i]) ? $tags[$i] : ''; + if (preg_match('/^([^\s]+)\s+(\$\w+\s+)?(.*)/s', $tag, $matches)) { + $type = $matches[1]; + $comment = $matches[3]; + } else { + $type = null; + $comment = $tag; } - } - if ($count < count($params)) { - for ($i = $count; $i < count($params); ++$i) { - $options[$params[$i]->getName()] = ''; + if ($param->isDefaultValueAvailable()) { + $optional[$name] = $this->formatOptionHelp('* ' . $name, false, $type, $param->getDefaultValue(), $comment); + } else { + $required[$name] = $this->formatOptionHelp('* ' . $name, true, $type, null, $comment); } } - ksort($options); - return $options; + return array($required, $optional); } /** - * @param \ReflectionClass $class - * @param Controller $controller - * @return array + * Returns the help information about the options available for a console controller. + * @param Controller $controller the console controller + * @return array the help information about the options */ - protected function getGlobalOptions($class, $controller) + protected function getOptionHelps($controller) { + $optionNames = $controller->globalOptions(); + if (empty($optionNames)) { + return array(); + } + + $class = new \ReflectionClass($controller); $options = array(); foreach ($class->getProperties() as $property) { - if (!$property->isPublic() || $property->isStatic() || $property->getDeclaringClass()->getName() !== get_class($controller)) { + $name = $property->getName(); + if (!in_array($name, $optionNames, true)) { continue; } - $name = $property->getName(); - $comment = strtr(trim(preg_replace('/^\s*\**( |\t)?/m', '', trim($property->getDocComment(), '/'))), "\r", ''); - if (preg_match('/^\s*@\w+/m', $comment, $matches, PREG_OFFSET_CAPTURE)) { - $meta = substr($comment, $matches[0][1]); + $defaultValue = $property->getValue($controller); + $tags = $this->parseComment($property->getDocComment()); + if (isset($tags['var']) || isset($tags['property'])) { + $doc = isset($tags['var']) ? $tags['var'] : $tags['property']; + if (is_array($doc)) { + $doc = reset($doc); + } + if (preg_match('/^([^\s]+)(.*)/s', $doc, $matches)) { + $type = $matches[1]; + $comment = $matches[2]; + } else { + $type = null; + $comment = $doc; + } + $options[$name] = $this->formatOptionHelp('--' . $name, false, $type, $defaultValue, $comment); } else { - $meta = ''; + $options[$name] = $this->formatOptionHelp('--' . $name, false, null, $defaultValue, ''); } - $tags = preg_split('/^\s*@/m', $meta, -1, PREG_SPLIT_NO_EMPTY); - foreach ($tags as $tag) { - $parts = preg_split('/\s+/', trim($tag), 2); - $comment = isset($parts[1]) ? $parts[1] : ''; - if ($parts[0] === 'var' || $parts[0] === 'property') { - if (preg_match('/^([^\s]+)(\s+.*)?/s', $comment, $matches)) { - $type = $matches[1]; - $doc = trim($matches[2]); - } else { - $type = $comment; - $doc = ''; - } - $comment = $type === '' ? '' : ($type); - if (trim($doc) !== '') { - $comment .= ', ' . preg_replace("/^/m", "", $doc); - } - $options[$name] = $comment; - break; + } + ksort($options); + return $options; + } + + /** + * Parses the comment block into tags. + * @param string $comment the comment block + * @return array the parsed tags + */ + protected function parseComment($comment) + { + $tags = array(); + $comment = "@description \n" . strtr(trim(preg_replace('/^\s*\**( |\t)?/m', '', trim($comment, '/'))), "\r", ''); + $parts = preg_split('/^\s*@/m', $comment, -1, PREG_SPLIT_NO_EMPTY); + foreach ($parts as $part) { + if (preg_match('/^(\w+)(.*)/ms', trim($part), $matches)) { + $name = $matches[1]; + if (!isset($tags[$name])) { + $tags[$name] = trim($matches[2]); + } elseif (is_array($tags[$name])) { + $tags[$name][] = trim($matches[2]); + } else { + $tags[$name] = array($tags[$name], trim($matches[2])); } } - if (!isset($options[$name])) { - $options[$name] = ''; + } + return $tags; + } + + /** + * Generates a well-formed string for an argument or option. + * @param string $name the name of the argument or option + * @param boolean $required whether the argument is required + * @param string $type the type of the option or argument + * @param mixed $defaultValue the default value of the option or argument + * @param string $comment comment about the option or argument + * @return string the formatted string for the argument or option + */ + protected function formatOptionHelp($name, $required, $type, $defaultValue, $comment) + { + $doc = ''; + $comment = trim($comment); + + if ($defaultValue !== null && !is_array($defaultValue)) { + if ($type === null) { + $type = gettype($defaultValue); } + $doc = "$type (defaults to " . var_export($defaultValue, true) . ")"; + } elseif (trim($type) !== '') { + $doc = $type; } - ksort($options); - return $options; + + if ($doc === '') { + $doc = $comment; + } elseif ($comment !== '') { + $doc .= "\n" . preg_replace("/^/m", " ", $comment); + } + + $name = $required ? "$name (required)" : $name; + return $doc === '' ? $name : "$name: $doc"; } } \ No newline at end of file diff --git a/framework/console/controllers/MessageController.php b/framework/console/controllers/MessageController.php index 2e8ec81..e010b55 100644 --- a/framework/console/controllers/MessageController.php +++ b/framework/console/controllers/MessageController.php @@ -1,10 +1,8 @@ <?php /** - * MessageController class file. - * * @author Qiang Xue <qiang.xue@gmail.com> * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ diff --git a/framework/console/controllers/MigrateController.php b/framework/console/controllers/MigrateController.php index e104856..3f816f1 100644 --- a/framework/console/controllers/MigrateController.php +++ b/framework/console/controllers/MigrateController.php @@ -1,539 +1,630 @@ -<?php -/** - * MigrateController class file. - * - * @author Qiang Xue <qiang.xue@gmail.com> - * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC - * @license http://www.yiiframework.com/license/ - */ - -namespace yii\console\controllers; - -use Yii; -use yii\console\Controller; - -/** - * This command provides support for database migrations. - * - * The implementation of this command and other supporting classes referenced - * the yii-dbmigrations extension ((https://github.com/pieterclaerhout/yii-dbmigrations), - * authored by Pieter Claerhout. - * - * EXAMPLES - * - * - yiic migrate - * Applies ALL new migrations. This is equivalent to 'yiic migrate up'. - * - * - yiic migrate create create_user_table - * Creates a new migration named 'create_user_table'. - * - * - yiic migrate up 3 - * Applies the next 3 new migrations. - * - * - yiic migrate down - * Reverts the last applied migration. - * - * - yiic migrate down 3 - * Reverts the last 3 applied migrations. - * - * - yiic migrate to 101129_185401 - * Migrates up or down to version 101129_185401. - * - * - yiic migrate mark 101129_185401 - * Modifies the migration history up or down to version 101129_185401. - * No actual migration will be performed. - * - * - yiic migrate history - * Shows all previously applied migration information. - * - * - yiic migrate history 10 - * Shows the last 10 applied migrations. - * - * - yiic migrate new - * Shows all new migrations. - * - * - yiic migrate new 10 - * Shows the next 10 migrations that have not been applied. - * - * @author Qiang Xue <qiang.xue@gmail.com> - * @since 2.0 - */ -class MigrateController extends Controller -{ - const BASE_MIGRATION = 'm000000_000000_base'; - - /** - * @var string the directory that stores the migrations. This must be specified - * in terms of a path alias, and the corresponding directory must exist. - * Defaults to 'application.migrations' (meaning 'protected/migrations'). - */ - public $migrationPath = '@application/migrations'; - /** - * @var string the name of the table for keeping applied migration information. - * This table will be automatically created if not exists. Defaults to 'tbl_migration'. - * The table structure is: (version varchar(255) primary key, apply_time integer) - */ - public $migrationTable = 'tbl_migration'; - /** - * @var string the application component ID that specifies the database connection for - * storing migration information. Defaults to 'db'. - */ - public $connectionID = 'db'; - /** - * @var string the path of the template file for generating new migrations. This - * must be specified in terms of a path alias (e.g. application.migrations.template). - * If not set, an internal template will be used. - */ - public $templateFile; - /** - * @var string the default command action. It defaults to 'up'. - */ - public $defaultAction = 'up'; - /** - * @var boolean whether to execute the migration in an interactive mode. Defaults to true. - * Set this to false when performing migration in a cron job or background process. - */ - public $interactive = true; - - - public function beforeAction($action) - { - if (parent::beforeAction($action)) { - $path = Yii::getAlias($this->migrationPath); - if ($path === false || !is_dir($path)) { - echo 'Error: the migration directory does not exist "' . $this->migrationPath . "\"\n"; - return false; - } - $this->migrationPath = $path; - $version = Yii::getVersion(); - echo "\nYii Migration Tool v2.0 (based on Yii v{$version})\n\n"; - return true; - } else { - return false; - } - } - - /** - * @param array $args - */ - public function actionUp($args) - { - if (($migrations = $this->getNewMigrations()) === array()) { - echo "No new migration found. Your system is up-to-date.\n"; - Yii::$application->end(); - } - - $total = count($migrations); - $step = isset($args[0]) ? (int)$args[0] : 0; - if ($step > 0) { - $migrations = array_slice($migrations, 0, $step); - } - - $n = count($migrations); - if ($n === $total) { - echo "Total $n new " . ($n === 1 ? 'migration' : 'migrations') . " to be applied:\n"; - } else { - echo "Total $n out of $total new " . ($total === 1 ? 'migration' : 'migrations') . " to be applied:\n"; - } - - foreach ($migrations as $migration) { - echo " $migration\n"; - } - echo "\n"; - - if ($this->confirm('Apply the above ' . ($n === 1 ? 'migration' : 'migrations') . "?")) { - foreach ($migrations as $migration) { - if ($this->migrateUp($migration) === false) { - echo "\nMigration failed. All later migrations are canceled.\n"; - return; - } - } - echo "\nMigrated up successfully.\n"; - } - } - - public function actionDown($args) - { - $step = isset($args[0]) ? (int)$args[0] : 1; - if ($step < 1) { - die("Error: The step parameter must be greater than 0.\n"); - } - - if (($migrations = $this->getMigrationHistory($step)) === array()) { - echo "No migration has been done before.\n"; - return; - } - $migrations = array_keys($migrations); - - $n = count($migrations); - echo "Total $n " . ($n === 1 ? 'migration' : 'migrations') . " to be reverted:\n"; - foreach ($migrations as $migration) { - echo " $migration\n"; - } - echo "\n"; - - if ($this->confirm('Revert the above ' . ($n === 1 ? 'migration' : 'migrations') . "?")) { - foreach ($migrations as $migration) { - if ($this->migrateDown($migration) === false) { - echo "\nMigration failed. All later migrations are canceled.\n"; - return; - } - } - echo "\nMigrated down successfully.\n"; - } - } - - public function actionRedo($args) - { - $step = isset($args[0]) ? (int)$args[0] : 1; - if ($step < 1) { - die("Error: The step parameter must be greater than 0.\n"); - } - - if (($migrations = $this->getMigrationHistory($step)) === array()) { - echo "No migration has been done before.\n"; - return; - } - $migrations = array_keys($migrations); - - $n = count($migrations); - echo "Total $n " . ($n === 1 ? 'migration' : 'migrations') . " to be redone:\n"; - foreach ($migrations as $migration) { - echo " $migration\n"; - } - echo "\n"; - - if ($this->confirm('Redo the above ' . ($n === 1 ? 'migration' : 'migrations') . "?")) { - foreach ($migrations as $migration) { - if ($this->migrateDown($migration) === false) { - echo "\nMigration failed. All later migrations are canceled.\n"; - return; - } - } - foreach (array_reverse($migrations) as $migration) { - if ($this->migrateUp($migration) === false) { - echo "\nMigration failed. All later migrations are canceled.\n"; - return; - } - } - echo "\nMigration redone successfully.\n"; - } - } - - public function actionTo($args) - { - if (isset($args[0])) { - $version = $args[0]; - } else { - $this->usageError('Please specify which version to migrate to.'); - } - - $originalVersion = $version; - if (preg_match('/^m?(\d{6}_\d{6})(_.*?)?$/', $version, $matches)) { - $version = 'm' . $matches[1]; - } else { - die("Error: The version option must be either a timestamp (e.g. 101129_185401)\nor the full name of a migration (e.g. m101129_185401_create_user_table).\n"); - } - - // try migrate up - $migrations = $this->getNewMigrations(); - foreach ($migrations as $i => $migration) { - if (strpos($migration, $version . '_') === 0) { - $this->actionUp(array($i + 1)); - return; - } - } - - // try migrate down - $migrations = array_keys($this->getMigrationHistory(-1)); - foreach ($migrations as $i => $migration) { - if (strpos($migration, $version . '_') === 0) { - if ($i === 0) { - echo "Already at '$originalVersion'. Nothing needs to be done.\n"; - } else { - $this->actionDown(array($i)); - } - return; - } - } - - die("Error: Unable to find the version '$originalVersion'.\n"); - } - - public function actionMark($args) - { - if (isset($args[0])) { - $version = $args[0]; - } else { - $this->usageError('Please specify which version to mark to.'); - } - $originalVersion = $version; - if (preg_match('/^m?(\d{6}_\d{6})(_.*?)?$/', $version, $matches)) { - $version = 'm' . $matches[1]; - } else { - die("Error: The version option must be either a timestamp (e.g. 101129_185401)\nor the full name of a migration (e.g. m101129_185401_create_user_table).\n"); - } - - $db = $this->getDb(); - - // try mark up - $migrations = $this->getNewMigrations(); - foreach ($migrations as $i => $migration) { - if (strpos($migration, $version . '_') === 0) { - if ($this->confirm("Set migration history at $originalVersion?")) { - $command = $db->createCommand(); - for ($j = 0; $j <= $i; ++$j) { - $command->insert($this->migrationTable, array( - 'version' => $migrations[$j], - 'apply_time' => time(), - )); - } - echo "The migration history is set at $originalVersion.\nNo actual migration was performed.\n"; - } - return; - } - } - - // try mark down - $migrations = array_keys($this->getMigrationHistory(-1)); - foreach ($migrations as $i => $migration) { - if (strpos($migration, $version . '_') === 0) { - if ($i === 0) { - echo "Already at '$originalVersion'. Nothing needs to be done.\n"; - } else { - if ($this->confirm("Set migration history at $originalVersion?")) { - $command = $db->createCommand(); - for ($j = 0; $j < $i; ++$j) { - $command->delete($this->migrationTable, $db->quoteColumnName('version') . '=:version', array(':version' => $migrations[$j])); - } - echo "The migration history is set at $originalVersion.\nNo actual migration was performed.\n"; - } - } - return; - } - } - - die("Error: Unable to find the version '$originalVersion'.\n"); - } - - public function actionHistory($args) - { - $limit = isset($args[0]) ? (int)$args[0] : -1; - $migrations = $this->getMigrationHistory($limit); - if ($migrations === array()) { - echo "No migration has been done before.\n"; - } else { - $n = count($migrations); - if ($limit > 0) { - echo "Showing the last $n applied " . ($n === 1 ? 'migration' : 'migrations') . ":\n"; - } else { - echo "Total $n " . ($n === 1 ? 'migration has' : 'migrations have') . " been applied before:\n"; - } - foreach ($migrations as $version => $time) { - echo " (" . date('Y-m-d H:i:s', $time) . ') ' . $version . "\n"; - } - } - } - - public function actionNew($args) - { - $limit = isset($args[0]) ? (int)$args[0] : -1; - $migrations = $this->getNewMigrations(); - if ($migrations === array()) { - echo "No new migrations found. Your system is up-to-date.\n"; - } else { - $n = count($migrations); - if ($limit > 0 && $n > $limit) { - $migrations = array_slice($migrations, 0, $limit); - echo "Showing $limit out of $n new " . ($n === 1 ? 'migration' : 'migrations') . ":\n"; - } else { - echo "Found $n new " . ($n === 1 ? 'migration' : 'migrations') . ":\n"; - } - - foreach ($migrations as $migration) { - echo " " . $migration . "\n"; - } - } - } - - public function actionCreate($args) - { - if (isset($args[0])) { - $name = $args[0]; - } else { - $this->usageError('Please provide the name of the new migration.'); - } - - if (!preg_match('/^\w+$/', $name)) { - die("Error: The name of the migration must contain letters, digits and/or underscore characters only.\n"); - } - - $name = 'm' . gmdate('ymd_His') . '_' . $name; - $content = strtr($this->getTemplate(), array('{ClassName}' => $name)); - $file = $this->migrationPath . DIRECTORY_SEPARATOR . $name . '.php'; - - if ($this->confirm("Create new migration '$file'?")) { - file_put_contents($file, $content); - echo "New migration created successfully.\n"; - } - } - - protected function migrateUp($class) - { - if ($class === self::BASE_MIGRATION) { - return; - } - - echo "*** applying $class\n"; - $start = microtime(true); - $migration = $this->instantiateMigration($class); - if ($migration->up() !== false) { - $this->getDb()->createCommand()->insert($this->migrationTable, array( - 'version' => $class, - 'apply_time' => time(), - )); - $time = microtime(true) - $start; - echo "*** applied $class (time: " . sprintf("%.3f", $time) . "s)\n\n"; - } else { - $time = microtime(true) - $start; - echo "*** failed to apply $class (time: " . sprintf("%.3f", $time) . "s)\n\n"; - return false; - } - } - - protected function migrateDown($class) - { - if ($class === self::BASE_MIGRATION) { - return; - } - - echo "*** reverting $class\n"; - $start = microtime(true); - $migration = $this->instantiateMigration($class); - if ($migration->down() !== false) { - $db = $this->getDb(); - $db->createCommand()->delete($this->migrationTable, $db->quoteColumnName('version') . '=:version', array(':version' => $class)); - $time = microtime(true) - $start; - echo "*** reverted $class (time: " . sprintf("%.3f", $time) . "s)\n\n"; - } else { - $time = microtime(true) - $start; - echo "*** failed to revert $class (time: " . sprintf("%.3f", $time) . "s)\n\n"; - return false; - } - } - - protected function instantiateMigration($class) - { - $file = $this->migrationPath . DIRECTORY_SEPARATOR . $class . '.php'; - require_once($file); - $migration = new $class; - $migration->setDb($this->getDb()); - return $migration; - } - - /** - * @var CDbConnection - */ - private $_db; - - protected function getDb() - { - if ($this->_db !== null) { - return $this->_db; - } else { - if (($this->_db = Yii::$application->getComponent($this->connectionID)) instanceof CDbConnection) { - return $this->_db; - } else { - die("Error: CMigrationCommand.connectionID '{$this->connectionID}' is invalid. Please make sure it refers to the ID of a CDbConnection application component.\n"); - } - } - } - - protected function getMigrationHistory($limit) - { - $db = $this->getDb(); - if ($db->schema->getTable($this->migrationTable) === null) { - $this->createMigrationHistoryTable(); - } - return CHtml::listData($db->createCommand() - ->select('version, apply_time') - ->from($this->migrationTable) - ->order('version DESC') - ->limit($limit) - ->queryAll(), 'version', 'apply_time'); - } - - protected function createMigrationHistoryTable() - { - $db = $this->getDb(); - echo 'Creating migration history table "' . $this->migrationTable . '"...'; - $db->createCommand()->createTable($this->migrationTable, array( - 'version' => 'string NOT NULL PRIMARY KEY', - 'apply_time' => 'integer', - )); - $db->createCommand()->insert($this->migrationTable, array( - 'version' => self::BASE_MIGRATION, - 'apply_time' => time(), - )); - echo "done.\n"; - } - - protected function getNewMigrations() - { - $applied = array(); - foreach ($this->getMigrationHistory(-1) as $version => $time) { - $applied[substr($version, 1, 13)] = true; - } - - $migrations = array(); - $handle = opendir($this->migrationPath); - while (($file = readdir($handle)) !== false) { - if ($file === '.' || $file === '..') { - continue; - } - $path = $this->migrationPath . DIRECTORY_SEPARATOR . $file; - if (preg_match('/^(m(\d{6}_\d{6})_.*?)\.php$/', $file, $matches) && is_file($path) && !isset($applied[$matches[2]])) { - $migrations[] = $matches[1]; - } - } - closedir($handle); - sort($migrations); - return $migrations; - } - - protected function getTemplate() - { - if ($this->templateFile !== null) { - return file_get_contents(Yii::getPathOfAlias($this->templateFile) . '.php'); - } else { - return <<<EOD -<?php - -class {ClassName} extends CDbMigration -{ - public function up() - { - } - - public function down() - { - echo "{ClassName} does not support migration down.\\n"; - return false; - } - - /* - // Use safeUp/safeDown to do migration with transaction - public function safeUp() - { - } - - public function safeDown() - { - } - */ -} -EOD; - } - } -} +<?php +/** + * @author Qiang Xue <qiang.xue@gmail.com> + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\console\controllers; + +use Yii; +use yii\console\Exception; +use yii\console\Controller; +use yii\db\Connection; +use yii\db\Query; +use yii\helpers\ArrayHelper; + +/** + * This command manages application migrations. + * + * A migration means a set of persistent changes to the application environment + * that is shared among different developers. For example, in an application + * backed by a database, a migration may refer to a set of changes to + * the database, such as creating a new table, adding a new table column. + * + * This command provides support for tracking the migration history, upgrading + * or downloading with migrations, and creating new migration skeletons. + * + * The migration history is stored in a database table named + * as [[migrationTable]]. The table will be automatically created the first time + * this command is executed, if it does not exist. You may also manually + * create it as follows: + * + * ~~~ + * CREATE TABLE tbl_migration ( + * version varchar(255) PRIMARY KEY, + * apply_time integer + * ) + * ~~~ + * + * Below are some common usages of this command: + * + * ~~~ + * # creates a new migration named 'create_user_table' + * yiic migrate/create create_user_table + * + * # applies ALL new migrations + * yiic migrate + * + * # reverts the last applied migration + * yiic migrate/down + * ~~~ + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class MigrateController extends Controller +{ + /** + * The name of the dummy migration that marks the beginning of the whole migration history. + */ + const BASE_MIGRATION = 'm000000_000000_base'; + + /** + * @var string the default command action. + */ + public $defaultAction = 'up'; + /** + * @var string the directory storing the migration classes. This can be either + * a path alias or a directory. + */ + public $migrationPath = '@app/migrations'; + /** + * @var string the name of the table for keeping applied migration information. + */ + public $migrationTable = 'tbl_migration'; + /** + * @var string the template file for generating new migrations. + * This can be either a path alias (e.g. "@app/migrations/template.php") + * or a file path. + */ + public $templateFile = '@yii/views/migration.php'; + /** + * @var boolean whether to execute the migration in an interactive mode. + */ + public $interactive = true; + /** + * @var Connection|string the DB connection object or the application + * component ID of the DB connection. + */ + public $db = 'db'; + + /** + * Returns the names of the global options for this command. + * @return array the names of the global options for this command. + */ + public function globalOptions() + { + return array('migrationPath', 'migrationTable', 'db', 'templateFile', 'interactive'); + } + + /** + * This method is invoked right before an action is to be executed (after all possible filters.) + * It checks the existence of the [[migrationPath]]. + * @param \yii\base\Action $action the action to be executed. + * @return boolean whether the action should continue to be executed. + * @throws Exception if the migration directory does not exist. + */ + public function beforeAction($action) + { + if (parent::beforeAction($action)) { + $path = Yii::getAlias($this->migrationPath); + if (!is_dir($path)) { + throw new Exception("The migration directory \"{$this->migrationPath}\" does not exist."); + } + $this->migrationPath = $path; + + if (is_string($this->db)) { + $this->db = Yii::$app->getComponent($this->db); + } + if (!$this->db instanceof Connection) { + throw new Exception("The 'db' option must refer to the application component ID of a DB connection."); + } + + $version = Yii::getVersion(); + echo "Yii Migration Tool (based on Yii v{$version})\n\n"; + return true; + } else { + return false; + } + } + + /** + * Upgrades the application by applying new migrations. + * For example, + * + * ~~~ + * yiic migrate # apply all new migrations + * yiic migrate 3 # apply the first 3 new migrations + * ~~~ + * + * @param integer $limit the number of new migrations to be applied. If 0, it means + * applying all available new migrations. + */ + public function actionUp($limit = 0) + { + if (($migrations = $this->getNewMigrations()) === array()) { + echo "No new migration found. Your system is up-to-date.\n"; + Yii::$app->end(); + } + + $total = count($migrations); + $limit = (int)$limit; + if ($limit > 0) { + $migrations = array_slice($migrations, 0, $limit); + } + + $n = count($migrations); + if ($n === $total) { + echo "Total $n new " . ($n === 1 ? 'migration' : 'migrations') . " to be applied:\n"; + } else { + echo "Total $n out of $total new " . ($total === 1 ? 'migration' : 'migrations') . " to be applied:\n"; + } + + foreach ($migrations as $migration) { + echo " $migration\n"; + } + echo "\n"; + + if ($this->confirm('Apply the above ' . ($n === 1 ? 'migration' : 'migrations') . "?")) { + foreach ($migrations as $migration) { + if (!$this->migrateUp($migration)) { + echo "\nMigration failed. The rest of the migrations are canceled.\n"; + return; + } + } + echo "\nMigrated up successfully.\n"; + } + } + + /** + * Downgrades the application by reverting old migrations. + * For example, + * + * ~~~ + * yiic migrate/down # revert the last migration + * yiic migrate/down 3 # revert the last 3 migrations + * ~~~ + * + * @param integer $limit the number of migrations to be reverted. Defaults to 1, + * meaning the last applied migration will be reverted. + * @throws Exception if the number of the steps specified is less than 1. + */ + public function actionDown($limit = 1) + { + $limit = (int)$limit; + if ($limit < 1) { + throw new Exception("The step argument must be greater than 0."); + } + + if (($migrations = $this->getMigrationHistory($limit)) === array()) { + echo "No migration has been done before.\n"; + return; + } + $migrations = array_keys($migrations); + + $n = count($migrations); + echo "Total $n " . ($n === 1 ? 'migration' : 'migrations') . " to be reverted:\n"; + foreach ($migrations as $migration) { + echo " $migration\n"; + } + echo "\n"; + + if ($this->confirm('Revert the above ' . ($n === 1 ? 'migration' : 'migrations') . "?")) { + foreach ($migrations as $migration) { + if (!$this->migrateDown($migration)) { + echo "\nMigration failed. The rest of the migrations are canceled.\n"; + return; + } + } + echo "\nMigrated down successfully.\n"; + } + } + + /** + * Redoes the last few migrations. + * + * This command will first revert the specified migrations, and then apply + * them again. For example, + * + * ~~~ + * yiic migrate/redo # redo the last applied migration + * yiic migrate/redo 3 # redo the last 3 applied migrations + * ~~~ + * + * @param integer $limit the number of migrations to be redone. Defaults to 1, + * meaning the last applied migration will be redone. + * @throws Exception if the number of the steps specified is less than 1. + */ + public function actionRedo($limit = 1) + { + $limit = (int)$limit; + if ($limit < 1) { + throw new Exception("The step argument must be greater than 0."); + } + + if (($migrations = $this->getMigrationHistory($limit)) === array()) { + echo "No migration has been done before.\n"; + return; + } + $migrations = array_keys($migrations); + + $n = count($migrations); + echo "Total $n " . ($n === 1 ? 'migration' : 'migrations') . " to be redone:\n"; + foreach ($migrations as $migration) { + echo " $migration\n"; + } + echo "\n"; + + if ($this->confirm('Redo the above ' . ($n === 1 ? 'migration' : 'migrations') . "?")) { + foreach ($migrations as $migration) { + if (!$this->migrateDown($migration)) { + echo "\nMigration failed. The rest of the migrations are canceled.\n"; + return; + } + } + foreach (array_reverse($migrations) as $migration) { + if (!$this->migrateUp($migration)) { + echo "\nMigration failed. The rest of the migrations migrations are canceled.\n"; + return; + } + } + echo "\nMigration redone successfully.\n"; + } + } + + /** + * Upgrades or downgrades till the specified version. + * + * This command will first revert the specified migrations, and then apply + * them again. For example, + * + * ~~~ + * yiic migrate/to 101129_185401 # using timestamp + * yiic migrate/to m101129_185401_create_user_table # using full name + * ~~~ + * + * @param string $version the version name that the application should be migrated to. + * This can be either the timestamp or the full name of the migration. + * @throws Exception if the version argument is invalid + */ + public function actionTo($version) + { + $originalVersion = $version; + if (preg_match('/^m?(\d{6}_\d{6})(_.*?)?$/', $version, $matches)) { + $version = 'm' . $matches[1]; + } else { + throw new Exception("The version argument must be either a timestamp (e.g. 101129_185401)\nor the full name of a migration (e.g. m101129_185401_create_user_table)."); + } + + // try migrate up + $migrations = $this->getNewMigrations(); + foreach ($migrations as $i => $migration) { + if (strpos($migration, $version . '_') === 0) { + $this->actionUp($i + 1); + return; + } + } + + // try migrate down + $migrations = array_keys($this->getMigrationHistory(-1)); + foreach ($migrations as $i => $migration) { + if (strpos($migration, $version . '_') === 0) { + if ($i === 0) { + echo "Already at '$originalVersion'. Nothing needs to be done.\n"; + } else { + $this->actionDown($i); + } + return; + } + } + + throw new Exception("Unable to find the version '$originalVersion'."); + } + + /** + * Modifies the migration history to the specified version. + * + * No actual migration will be performed. + * + * ~~~ + * yiic migrate/mark 101129_185401 # using timestamp + * yiic migrate/mark m101129_185401_create_user_table # using full name + * ~~~ + * + * @param string $version the version at which the migration history should be marked. + * This can be either the timestamp or the full name of the migration. + * @throws Exception if the version argument is invalid or the version cannot be found. + */ + public function actionMark($version) + { + $originalVersion = $version; + if (preg_match('/^m?(\d{6}_\d{6})(_.*?)?$/', $version, $matches)) { + $version = 'm' . $matches[1]; + } else { + throw new Exception("The version argument must be either a timestamp (e.g. 101129_185401)\nor the full name of a migration (e.g. m101129_185401_create_user_table)."); + } + + // try mark up + $migrations = $this->getNewMigrations(); + foreach ($migrations as $i => $migration) { + if (strpos($migration, $version . '_') === 0) { + if ($this->confirm("Set migration history at $originalVersion?")) { + $command = $this->db->createCommand(); + for ($j = 0; $j <= $i; ++$j) { + $command->insert($this->migrationTable, array( + 'version' => $migrations[$j], + 'apply_time' => time(), + ))->execute(); + } + echo "The migration history is set at $originalVersion.\nNo actual migration was performed.\n"; + } + return; + } + } + + // try mark down + $migrations = array_keys($this->getMigrationHistory(-1)); + foreach ($migrations as $i => $migration) { + if (strpos($migration, $version . '_') === 0) { + if ($i === 0) { + echo "Already at '$originalVersion'. Nothing needs to be done.\n"; + } else { + if ($this->confirm("Set migration history at $originalVersion?")) { + $command = $this->db->createCommand(); + for ($j = 0; $j < $i; ++$j) { + $command->delete($this->migrationTable, array( + 'version' => $migrations[$j], + ))->execute(); + } + echo "The migration history is set at $originalVersion.\nNo actual migration was performed.\n"; + } + } + return; + } + } + + throw new Exception("Unable to find the version '$originalVersion'."); + } + + /** + * Displays the migration history. + * + * This command will show the list of migrations that have been applied + * so far. For example, + * + * ~~~ + * yiic migrate/history # showing the last 10 migrations + * yiic migrate/history 5 # showing the last 5 migrations + * yiic migrate/history 0 # showing the whole history + * ~~~ + * + * @param integer $limit the maximum number of migrations to be displayed. + * If it is 0, the whole migration history will be displayed. + */ + public function actionHistory($limit = 10) + { + $limit = (int)$limit; + $migrations = $this->getMigrationHistory($limit); + if ($migrations === array()) { + echo "No migration has been done before.\n"; + } else { + $n = count($migrations); + if ($limit > 0) { + echo "Showing the last $n applied " . ($n === 1 ? 'migration' : 'migrations') . ":\n"; + } else { + echo "Total $n " . ($n === 1 ? 'migration has' : 'migrations have') . " been applied before:\n"; + } + foreach ($migrations as $version => $time) { + echo " (" . date('Y-m-d H:i:s', $time) . ') ' . $version . "\n"; + } + } + } + + /** + * Displays the un-applied new migrations. + * + * This command will show the new migrations that have not been applied. + * For example, + * + * ~~~ + * yiic migrate/new # showing the first 10 new migrations + * yiic migrate/new 5 # showing the first 5 new migrations + * yiic migrate/new 0 # showing all new migrations + * ~~~ + * + * @param integer $limit the maximum number of new migrations to be displayed. + * If it is 0, all available new migrations will be displayed. + */ + public function actionNew($limit = 10) + { + $limit = (int)$limit; + $migrations = $this->getNewMigrations(); + if ($migrations === array()) { + echo "No new migrations found. Your system is up-to-date.\n"; + } else { + $n = count($migrations); + if ($limit > 0 && $n > $limit) { + $migrations = array_slice($migrations, 0, $limit); + echo "Showing $limit out of $n new " . ($n === 1 ? 'migration' : 'migrations') . ":\n"; + } else { + echo "Found $n new " . ($n === 1 ? 'migration' : 'migrations') . ":\n"; + } + + foreach ($migrations as $migration) { + echo " " . $migration . "\n"; + } + } + } + + /** + * Creates a new migration. + * + * This command creates a new migration using the available migration template. + * After using this command, developers should modify the created migration + * skeleton by filling up the actual migration logic. + * + * ~~~ + * yiic migrate/create create_user_table + * ~~~ + * + * @param string $name the name of the new migration. This should only contain + * letters, digits and/or underscores. + * @throws Exception if the name argument is invalid. + */ + public function actionCreate($name) + { + if (!preg_match('/^\w+$/', $name)) { + throw new Exception("The migration name should contain letters, digits and/or underscore characters only."); + } + + $name = 'm' . gmdate('ymd_His') . '_' . $name; + $file = $this->migrationPath . DIRECTORY_SEPARATOR . $name . '.php'; + + if ($this->confirm("Create new migration '$file'?")) { + $content = $this->renderFile(Yii::getAlias($this->templateFile), array( + 'className' => $name, + )); + file_put_contents($file, $content); + echo "New migration created successfully.\n"; + } + } + + /** + * Upgrades with the specified migration class. + * @param string $class the migration class name + * @return boolean whether the migration is successful + */ + protected function migrateUp($class) + { + if ($class === self::BASE_MIGRATION) { + return true; + } + + echo "*** applying $class\n"; + $start = microtime(true); + $migration = $this->createMigration($class); + if ($migration->up() !== false) { + $this->db->createCommand()->insert($this->migrationTable, array( + 'version' => $class, + 'apply_time' => time(), + ))->execute(); + $time = microtime(true) - $start; + echo "*** applied $class (time: " . sprintf("%.3f", $time) . "s)\n\n"; + return true; + } else { + $time = microtime(true) - $start; + echo "*** failed to apply $class (time: " . sprintf("%.3f", $time) . "s)\n\n"; + return false; + } + } + + /** + * Downgrades with the specified migration class. + * @param string $class the migration class name + * @return boolean whether the migration is successful + */ + protected function migrateDown($class) + { + if ($class === self::BASE_MIGRATION) { + return true; + } + + echo "*** reverting $class\n"; + $start = microtime(true); + $migration = $this->createMigration($class); + if ($migration->down() !== false) { + $this->db->createCommand()->delete($this->migrationTable, array( + 'version' => $class, + ))->execute(); + $time = microtime(true) - $start; + echo "*** reverted $class (time: " . sprintf("%.3f", $time) . "s)\n\n"; + return true; + } else { + $time = microtime(true) - $start; + echo "*** failed to revert $class (time: " . sprintf("%.3f", $time) . "s)\n\n"; + return false; + } + } + + /** + * Creates a new migration instance. + * @param string $class the migration class name + * @return \yii\db\Migration the migration instance + */ + protected function createMigration($class) + { + $file = $this->migrationPath . DIRECTORY_SEPARATOR . $class . '.php'; + require_once($file); + return new $class(array( + 'db' => $this->db, + )); + } + + /** + * Returns the migration history. + * @param integer $limit the maximum number of records in the history to be returned + * @return array the migration history + */ + protected function getMigrationHistory($limit) + { + if ($this->db->schema->getTableSchema($this->migrationTable) === null) { + $this->createMigrationHistoryTable(); + } + $query = new Query; + $rows = $query->select(array('version', 'apply_time')) + ->from($this->migrationTable) + ->orderBy('version DESC') + ->limit($limit) + ->createCommand() + ->queryAll(); + $history = ArrayHelper::map($rows, 'version', 'apply_time'); + unset($history[self::BASE_MIGRATION]); + return $history; + } + + /** + * Creates the migration history table. + */ + protected function createMigrationHistoryTable() + { + echo 'Creating migration history table "' . $this->migrationTable . '"...'; + $this->db->createCommand()->createTable($this->migrationTable, array( + 'version' => 'varchar(255) NOT NULL PRIMARY KEY', + 'apply_time' => 'integer', + ))->execute(); + $this->db->createCommand()->insert($this->migrationTable, array( + 'version' => self::BASE_MIGRATION, + 'apply_time' => time(), + ))->execute(); + echo "done.\n"; + } + + /** + * Returns the migrations that are not applied. + * @return array list of new migrations + */ + protected function getNewMigrations() + { + $applied = array(); + foreach ($this->getMigrationHistory(-1) as $version => $time) { + $applied[substr($version, 1, 13)] = true; + } + + $migrations = array(); + $handle = opendir($this->migrationPath); + while (($file = readdir($handle)) !== false) { + if ($file === '.' || $file === '..') { + continue; + } + $path = $this->migrationPath . DIRECTORY_SEPARATOR . $file; + if (preg_match('/^(m(\d{6}_\d{6})_.*?)\.php$/', $file, $matches) && is_file($path) && !isset($applied[$matches[2]])) { + $migrations[] = $matches[1]; + } + } + closedir($handle); + sort($migrations); + return $migrations; + } +} diff --git a/framework/console/create/config.php b/framework/console/webapp/config.php similarity index 94% rename from framework/console/create/config.php rename to framework/console/webapp/config.php index 29f0b0b..112fb18 100644 --- a/framework/console/create/config.php +++ b/framework/console/webapp/config.php @@ -1,5 +1,5 @@ <?php -/** @var $controller \yii\console\controllers\CreateController */ +/** @var $controller \yii\console\controllers\AppController */ $controller = $this; return array( diff --git a/framework/console/create/default/index.php b/framework/console/webapp/default/index.php similarity index 100% rename from framework/console/create/default/index.php rename to framework/console/webapp/default/index.php diff --git a/framework/console/create/default/protected/config/main.php b/framework/console/webapp/default/protected/config/main.php similarity index 100% rename from framework/console/create/default/protected/config/main.php rename to framework/console/webapp/default/protected/config/main.php diff --git a/framework/console/create/default/protected/controllers/SiteController.php b/framework/console/webapp/default/protected/controllers/SiteController.php similarity index 100% rename from framework/console/create/default/protected/controllers/SiteController.php rename to framework/console/webapp/default/protected/controllers/SiteController.php diff --git a/framework/console/create/default/protected/views/layouts/main.php b/framework/console/webapp/default/protected/views/layouts/main.php similarity index 100% rename from framework/console/create/default/protected/views/layouts/main.php rename to framework/console/webapp/default/protected/views/layouts/main.php diff --git a/framework/console/create/default/protected/views/site/index.php b/framework/console/webapp/default/protected/views/site/index.php similarity index 100% rename from framework/console/create/default/protected/views/site/index.php rename to framework/console/webapp/default/protected/views/site/index.php diff --git a/framework/db/ActiveQuery.php b/framework/db/ActiveQuery.php index de56e14..43c3059 100644 --- a/framework/db/ActiveQuery.php +++ b/framework/db/ActiveQuery.php @@ -1,10 +1,8 @@ <?php /** - * ActiveQuery class file. - * * @author Qiang Xue <qiang.xue@gmail.com> * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ diff --git a/framework/db/ActiveRecord.php b/framework/db/ActiveRecord.php index c6d3d81..d8f2f65 100644 --- a/framework/db/ActiveRecord.php +++ b/framework/db/ActiveRecord.php @@ -1,24 +1,22 @@ <?php /** - * ActiveRecord class file. - * * @author Qiang Xue <qiang.xue@gmail.com> * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ namespace yii\db; use yii\base\Model; -use yii\base\Event; +use yii\base\InvalidParamException; use yii\base\ModelEvent; use yii\base\UnknownMethodException; use yii\base\InvalidCallException; use yii\db\Connection; use yii\db\TableSchema; use yii\db\Expression; -use yii\util\StringHelper; +use yii\helpers\StringHelper; /** * ActiveRecord is the base class for classes representing relational data in terms of objects. @@ -96,7 +94,7 @@ class ActiveRecord extends Model */ public static function getDb() { - return \Yii::$application->getDb(); + return \Yii::$app->getDb(); } /** @@ -849,7 +847,7 @@ class ActiveRecord extends Model */ public function beforeSave($insert) { - $event = new ModelEvent($this); + $event = new ModelEvent; $this->trigger($insert ? self::EVENT_BEFORE_INSERT : self::EVENT_BEFORE_UPDATE, $event); return $event->isValid; } @@ -889,7 +887,7 @@ class ActiveRecord extends Model */ public function beforeDelete() { - $event = new ModelEvent($this); + $event = new ModelEvent; $this->trigger(self::EVENT_BEFORE_DELETE, $event); return $event->isValid; } @@ -1045,7 +1043,7 @@ class ActiveRecord extends Model * It can be declared in either the Active Record class itself or one of its behaviors. * @param string $name the relation name * @return ActiveRelation the relation object - * @throws InvalidCallException if the named relation does not exist. + * @throws InvalidParamException if the named relation does not exist. */ public function getRelation($name) { @@ -1057,7 +1055,7 @@ class ActiveRecord extends Model } } catch (UnknownMethodException $e) { } - throw new InvalidCallException(get_class($this) . ' has no relation named "' . $name . '".'); + throw new InvalidParamException(get_class($this) . ' has no relation named "' . $name . '".'); } /** diff --git a/framework/db/ActiveRelation.php b/framework/db/ActiveRelation.php index 4d87fb3..c547f1a 100644 --- a/framework/db/ActiveRelation.php +++ b/framework/db/ActiveRelation.php @@ -1,10 +1,8 @@ <?php /** - * ActiveRelation class file. - * * @author Qiang Xue <qiang.xue@gmail.com> * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ @@ -57,16 +55,16 @@ class ActiveRelation extends ActiveQuery /** * Specifies the relation associated with the pivot table. * @param string $relationName the relation name. This refers to a relation declared in [[primaryModel]]. - * @param callback $callback a PHP callback for customizing the relation associated with the pivot table. + * @param callable $callable a PHP callback for customizing the relation associated with the pivot table. * Its signature should be `function($query)`, where `$query` is the query to be customized. * @return ActiveRelation the relation object itself. */ - public function via($relationName, $callback = null) + public function via($relationName, $callable = null) { $relation = $this->primaryModel->getRelation($relationName); $this->via = array($relationName, $relation); - if ($callback !== null) { - call_user_func($callback, $relation); + if ($callable !== null) { + call_user_func($callable, $relation); } return $this; } @@ -77,11 +75,11 @@ class ActiveRelation extends ActiveQuery * @param array $link the link between the pivot table and the table associated with [[primaryModel]]. * The keys of the array represent the columns in the pivot table, and the values represent the columns * in the [[primaryModel]] table. - * @param callback $callback a PHP callback for customizing the relation associated with the pivot table. + * @param callable $callable a PHP callback for customizing the relation associated with the pivot table. * Its signature should be `function($query)`, where `$query` is the query to be customized. * @return ActiveRelation */ - public function viaTable($tableName, $link, $callback = null) + public function viaTable($tableName, $link, $callable = null) { $relation = new ActiveRelation(array( 'modelClass' => get_class($this->primaryModel), @@ -91,8 +89,8 @@ class ActiveRelation extends ActiveQuery 'asArray' => true, )); $this->via = $relation; - if ($callback !== null) { - call_user_func($callback, $relation); + if ($callable !== null) { + call_user_func($callable, $relation); } return $this; } diff --git a/framework/db/ColumnSchema.php b/framework/db/ColumnSchema.php index 44e6cb0..ffdafd4 100644 --- a/framework/db/ColumnSchema.php +++ b/framework/db/ColumnSchema.php @@ -1,9 +1,7 @@ <?php /** - * ColumnSchema class file. - * * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ diff --git a/framework/db/Command.php b/framework/db/Command.php index 3531fa7..ecd3674 100644 --- a/framework/db/Command.php +++ b/framework/db/Command.php @@ -1,15 +1,15 @@ <?php /** - * Command class file. - * * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ namespace yii\db; +use Yii; use yii\base\NotSupportedException; +use yii\caching\Cache; /** * Command represents a SQL statement to be executed against a database. @@ -134,9 +134,9 @@ class Command extends \yii\base\Component try { $this->pdoStatement = $this->db->pdo->prepare($sql); } catch (\Exception $e) { - \Yii::error($e->getMessage() . "\nFailed to prepare SQL: $sql", __CLASS__); + Yii::error($e->getMessage() . "\nFailed to prepare SQL: $sql", __CLASS__); $errorInfo = $e instanceof \PDOException ? $e->errorInfo : null; - throw new Exception($e->getMessage(), (int)$e->getCode(), $errorInfo); + throw new Exception($e->getMessage(), $errorInfo, (int)$e->getCode()); } } } @@ -253,24 +253,20 @@ class Command extends \yii\base\Component * Executes the SQL statement. * This method should only be used for executing non-query SQL statement, such as `INSERT`, `DELETE`, `UPDATE` SQLs. * No result set will be returned. - * @param array $params input parameters (name=>value) for the SQL execution. This is an alternative - * to [[bindValues()]]. Note that if you pass parameters in this way, any previous call to [[bindParam()]] - * or [[bindValue()]] will be ignored. * @return integer number of rows affected by the execution. * @throws Exception execution failed */ - public function execute($params = array()) + public function execute() { $sql = $this->getSql(); - $this->_params = array_merge($this->_params, $params); if ($this->_params === array()) { $paramLog = ''; } else { $paramLog = "\nParameters: " . var_export($this->_params, true); } - \Yii::trace("Executing SQL: {$sql}{$paramLog}", __CLASS__); + Yii::trace("Executing SQL: {$sql}{$paramLog}", __CLASS__); if ($sql == '') { return 0; @@ -278,94 +274,78 @@ class Command extends \yii\base\Component try { if ($this->db->enableProfiling) { - \Yii::beginProfile(__METHOD__ . "($sql)", __CLASS__); + Yii::beginProfile(__METHOD__ . "($sql)", __CLASS__); } $this->prepare(); - if ($params === array()) { - $this->pdoStatement->execute(); - } else { - $this->pdoStatement->execute($params); - } + $this->pdoStatement->execute(); $n = $this->pdoStatement->rowCount(); if ($this->db->enableProfiling) { - \Yii::endProfile(__METHOD__ . "($sql)", __CLASS__); + Yii::endProfile(__METHOD__ . "($sql)", __CLASS__); } return $n; } catch (\Exception $e) { if ($this->db->enableProfiling) { - \Yii::endProfile(__METHOD__ . "($sql)", __CLASS__); + Yii::endProfile(__METHOD__ . "($sql)", __CLASS__); } $message = $e->getMessage(); - \Yii::error("$message\nFailed to execute SQL: {$sql}{$paramLog}", __CLASS__); + Yii::error("$message\nFailed to execute SQL: {$sql}{$paramLog}", __CLASS__); $errorInfo = $e instanceof \PDOException ? $e->errorInfo : null; - throw new Exception($message, (int)$e->getCode(), $errorInfo); + throw new Exception($message, $errorInfo, (int)$e->getCode()); } } /** * Executes the SQL statement and returns query result. * This method is for executing a SQL query that returns result set, such as `SELECT`. - * @param array $params input parameters (name=>value) for the SQL execution. This is an alternative - * to [[bindValues()]]. Note that if you pass parameters in this way, any previous call to [[bindParam()]] - * or [[bindValue()]] will be ignored. * @return DataReader the reader object for fetching the query result * @throws Exception execution failed */ - public function query($params = array()) + public function query() { - return $this->queryInternal('', $params); + return $this->queryInternal(''); } /** * Executes the SQL statement and returns ALL rows at once. - * @param array $params input parameters (name=>value) for the SQL execution. This is an alternative - * to [[bindValues()]]. Note that if you pass parameters in this way, any previous call to [[bindParam()]] - * or [[bindValue()]] will be ignored. * @param mixed $fetchMode the result fetch mode. Please refer to [PHP manual](http://www.php.net/manual/en/function.PDOStatement-setFetchMode.php) * for valid fetch modes. If this parameter is null, the value set in [[fetchMode]] will be used. * @return array all rows of the query result. Each array element is an array representing a row of data. * An empty array is returned if the query results in nothing. * @throws Exception execution failed */ - public function queryAll($params = array(), $fetchMode = null) + public function queryAll($fetchMode = null) { - return $this->queryInternal('fetchAll', $params, $fetchMode); + return $this->queryInternal('fetchAll', $fetchMode); } /** * Executes the SQL statement and returns the first row of the result. * This method is best used when only the first row of result is needed for a query. - * @param array $params input parameters (name=>value) for the SQL execution. This is an alternative - * to [[bindValues()]]. Note that if you pass parameters in this way, any previous call to [[bindParam()]] - * or [[bindValue()]] will be ignored. * @param mixed $fetchMode the result fetch mode. Please refer to [PHP manual](http://www.php.net/manual/en/function.PDOStatement-setFetchMode.php) * for valid fetch modes. If this parameter is null, the value set in [[fetchMode]] will be used. * @return array|boolean the first row (in terms of an array) of the query result. False is returned if the query * results in nothing. * @throws Exception execution failed */ - public function queryRow($params = array(), $fetchMode = null) + public function queryRow($fetchMode = null) { - return $this->queryInternal('fetch', $params, $fetchMode); + return $this->queryInternal('fetch', $fetchMode); } /** * Executes the SQL statement and returns the value of the first column in the first row of data. * This method is best used when only a single value is needed for a query. - * @param array $params input parameters (name=>value) for the SQL execution. This is an alternative - * to [[bindValues()]]. Note that if you pass parameters in this way, any previous call to [[bindParam()]] - * or [[bindValue()]] will be ignored. * @return string|boolean the value of the first column in the first row of the query result. * False is returned if there is no value. * @throws Exception execution failed */ - public function queryScalar($params = array()) + public function queryScalar() { - $result = $this->queryInternal('fetchColumn', $params, 0); + $result = $this->queryInternal('fetchColumn', 0); if (is_resource($result) && get_resource_type($result) === 'stream') { return stream_get_contents($result); } else { @@ -377,65 +357,60 @@ class Command extends \yii\base\Component * Executes the SQL statement and returns the first column of the result. * This method is best used when only the first column of result (i.e. the first element in each row) * is needed for a query. - * @param array $params input parameters (name=>value) for the SQL execution. This is an alternative - * to [[bindValues()]]. Note that if you pass parameters in this way, any previous call to [[bindParam()]] - * or [[bindValue()]] will be ignored. * @return array the first column of the query result. Empty array is returned if the query results in nothing. * @throws Exception execution failed */ - public function queryColumn($params = array()) + public function queryColumn() { - return $this->queryInternal('fetchAll', $params, \PDO::FETCH_COLUMN); + return $this->queryInternal('fetchAll', \PDO::FETCH_COLUMN); } /** * Performs the actual DB query of a SQL statement. * @param string $method method of PDOStatement to be called - * @param array $params input parameters (name=>value) for the SQL execution. This is an alternative - * to [[bindValues()]]. Note that if you pass parameters in this way, any previous call to [[bindParam()]] - * or [[bindValue()]] will be ignored. * @param mixed $fetchMode the result fetch mode. Please refer to [PHP manual](http://www.php.net/manual/en/function.PDOStatement-setFetchMode.php) * for valid fetch modes. If this parameter is null, the value set in [[fetchMode]] will be used. * @return mixed the method execution result * @throws Exception if the query causes any problem */ - private function queryInternal($method, $params, $fetchMode = null) + private function queryInternal($method, $fetchMode = null) { $db = $this->db; $sql = $this->getSql(); - $this->_params = array_merge($this->_params, $params); if ($this->_params === array()) { $paramLog = ''; } else { $paramLog = "\nParameters: " . var_export($this->_params, true); } - \Yii::trace("Querying SQL: {$sql}{$paramLog}", __CLASS__); + Yii::trace("Querying SQL: {$sql}{$paramLog}", __CLASS__); /** @var $cache \yii\caching\Cache */ if ($db->enableQueryCache && $method !== '') { - $cache = \Yii::$application->getComponent($db->queryCacheID); + $cache = is_string($db->queryCache) ? Yii::$app->getComponent($db->queryCache) : $db->queryCache; } - if (isset($cache)) { - $cacheKey = $cache->buildKey(__CLASS__, $db->dsn, $db->username, $sql, $paramLog); + if (isset($cache) && $cache instanceof Cache) { + $cacheKey = $cache->buildKey(array( + __CLASS__, + $db->dsn, + $db->username, + $sql, + $paramLog, + )); if (($result = $cache->get($cacheKey)) !== false) { - \Yii::trace('Query result found in cache', __CLASS__); + Yii::trace('Query result served from cache', __CLASS__); return $result; } } try { if ($db->enableProfiling) { - \Yii::beginProfile(__METHOD__ . "($sql)", __CLASS__); + Yii::beginProfile(__METHOD__ . "($sql)", __CLASS__); } $this->prepare(); - if ($params === array()) { - $this->pdoStatement->execute(); - } else { - $this->pdoStatement->execute($params); - } + $this->pdoStatement->execute(); if ($method === '') { $result = new DataReader($this); @@ -448,23 +423,23 @@ class Command extends \yii\base\Component } if ($db->enableProfiling) { - \Yii::endProfile(__METHOD__ . "($sql)", __CLASS__); + Yii::endProfile(__METHOD__ . "($sql)", __CLASS__); } - if (isset($cache, $cacheKey)) { + if (isset($cache, $cacheKey) && $cache instanceof Cache) { $cache->set($cacheKey, $result, $db->queryCacheDuration, $db->queryCacheDependency); - \Yii::trace('Saved query result in cache', __CLASS__); + Yii::trace('Saved query result in cache', __CLASS__); } return $result; } catch (\Exception $e) { if ($db->enableProfiling) { - \Yii::endProfile(__METHOD__ . "($sql)", __CLASS__); + Yii::endProfile(__METHOD__ . "($sql)", __CLASS__); } $message = $e->getMessage(); - \Yii::error("$message\nCommand::$method() failed: {$sql}{$paramLog}", __CLASS__); + Yii::error("$message\nCommand::$method() failed: {$sql}{$paramLog}", __CLASS__); $errorInfo = $e instanceof \PDOException ? $e->errorInfo : null; - throw new Exception($message, (int)$e->getCode(), $errorInfo); + throw new Exception($message, $errorInfo, (int)$e->getCode()); } } diff --git a/framework/db/Connection.php b/framework/db/Connection.php index 3564361..59e8422 100644 --- a/framework/db/Connection.php +++ b/framework/db/Connection.php @@ -1,9 +1,7 @@ <?php /** - * Connection class file - * * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ @@ -12,6 +10,7 @@ namespace yii\db; use yii\base\Component; use yii\base\InvalidConfigException; use yii\base\NotSupportedException; +use yii\caching\Cache; /** * Connection represents a connection to a database via [PDO](http://www.php.net/manual/en/ref.pdo.php). @@ -138,10 +137,10 @@ class Connection extends Component /** * @var boolean whether to enable schema caching. * Note that in order to enable truly schema caching, a valid cache component as specified - * by [[schemaCacheID]] must be enabled and [[enableSchemaCache]] must be set true. + * by [[schemaCache]] must be enabled and [[enableSchemaCache]] must be set true. * @see schemaCacheDuration * @see schemaCacheExclude - * @see schemaCacheID + * @see schemaCache */ public $enableSchemaCache = false; /** @@ -157,20 +156,20 @@ class Connection extends Component */ public $schemaCacheExclude = array(); /** - * @var string the ID of the cache application component that is used to cache the table metadata. - * Defaults to 'cache'. + * @var Cache|string the cache object or the ID of the cache application component that + * is used to cache the table metadata. * @see enableSchemaCache */ - public $schemaCacheID = 'cache'; + public $schemaCache = 'cache'; /** * @var boolean whether to enable query caching. * Note that in order to enable query caching, a valid cache component as specified - * by [[queryCacheID]] must be enabled and [[enableQueryCache]] must be set true. + * by [[queryCache]] must be enabled and [[enableQueryCache]] must be set true. * * Methods [[beginCache()]] and [[endCache()]] can be used as shortcuts to turn on * and off query caching on the fly. * @see queryCacheDuration - * @see queryCacheID + * @see queryCache * @see queryCacheDependency * @see beginCache() * @see endCache() @@ -178,7 +177,7 @@ class Connection extends Component public $enableQueryCache = false; /** * @var integer number of seconds that query results can remain valid in cache. - * Defaults to 3600, meaning one hour. + * Defaults to 3600, meaning 3600 seconds, or one hour. * Use 0 to indicate that the cached data will never expire. * @see enableQueryCache */ @@ -190,11 +189,11 @@ class Connection extends Component */ public $queryCacheDependency; /** - * @var string the ID of the cache application component that is used for query caching. - * Defaults to 'cache'. + * @var Cache|string the cache object or the ID of the cache application component + * that is used for query caching. * @see enableQueryCache */ - public $queryCacheID = 'cache'; + public $queryCache = 'cache'; /** * @var string the charset used for database connection. The property is only used * for MySQL and PostgreSQL databases. Defaults to null, meaning using default charset @@ -292,7 +291,7 @@ class Connection extends Component * This method is provided as a shortcut to setting two properties that are related * with query caching: [[queryCacheDuration]] and [[queryCacheDependency]]. * @param integer $duration the number of seconds that query results may remain valid in cache. - * See [[queryCacheDuration]] for more details. + * If not set, it will use the value of [[queryCacheDuration]]. See [[queryCacheDuration]] for more details. * @param \yii\caching\Dependency $dependency the dependency for the cached query result. * See [[queryCacheDependency]] for more details. */ @@ -322,7 +321,7 @@ class Connection extends Component { if ($this->pdo === null) { if (empty($this->dsn)) { - throw new InvalidConfigException('Connection.dsn cannot be empty.'); + throw new InvalidConfigException('Connection::dsn cannot be empty.'); } try { \Yii::trace('Opening DB connection: ' . $this->dsn, __CLASS__); @@ -332,7 +331,7 @@ class Connection extends Component catch (\PDOException $e) { \Yii::error("Failed to open DB connection ({$this->dsn}): " . $e->getMessage(), __CLASS__); $message = YII_DEBUG ? 'Failed to open DB connection: ' . $e->getMessage() : 'Failed to open DB connection.'; - throw new Exception($message, (int)$e->getCode(), $e->errorInfo); + throw new Exception($message, $e->errorInfo, (int)$e->getCode()); } } } diff --git a/framework/db/DataReader.php b/framework/db/DataReader.php index 8e5291e..20444e7 100644 --- a/framework/db/DataReader.php +++ b/framework/db/DataReader.php @@ -1,9 +1,7 @@ <?php /** - * DataReader class file - * * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ diff --git a/framework/db/Exception.php b/framework/db/Exception.php index 209dc40..ad97b5a 100644 --- a/framework/db/Exception.php +++ b/framework/db/Exception.php @@ -1,9 +1,7 @@ <?php /** - * Exception class file. - * * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ @@ -26,13 +24,14 @@ class Exception extends \yii\base\Exception /** * Constructor. * @param string $message PDO error message - * @param integer $code PDO error code * @param mixed $errorInfo PDO error info + * @param integer $code PDO error code + * @param \Exception $previous The previous exception used for the exception chaining. */ - public function __construct($message, $code = 0, $errorInfo = null) + public function __construct($message, $errorInfo = null, $code = 0, \Exception $previous = null) { $this->errorInfo = $errorInfo; - parent::__construct($message, $code); + parent::__construct($message, $code, $previous); } /** @@ -40,6 +39,6 @@ class Exception extends \yii\base\Exception */ public function getName() { - return \Yii::t('yii', 'Database Exception'); + return \Yii::t('yii|Database Exception'); } } \ No newline at end of file diff --git a/framework/db/Expression.php b/framework/db/Expression.php index 23fb13e..4ebcd5f 100644 --- a/framework/db/Expression.php +++ b/framework/db/Expression.php @@ -1,9 +1,7 @@ <?php /** - * Expression class file. - * * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ diff --git a/framework/db/Migration.php b/framework/db/Migration.php index 6dbaa78..ce2cf97 100644 --- a/framework/db/Migration.php +++ b/framework/db/Migration.php @@ -1,9 +1,7 @@ <?php /** - * Migration class file. - * * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ @@ -51,7 +49,7 @@ class Migration extends \yii\base\Component { parent::init(); if ($this->db === null) { - $this->db = \Yii::$application->getComponent('db'); + $this->db = \Yii::$app->getComponent('db'); } } diff --git a/framework/db/Query.php b/framework/db/Query.php index 10bba08..2239f5d 100644 --- a/framework/db/Query.php +++ b/framework/db/Query.php @@ -1,9 +1,7 @@ <?php /** - * Query class file. - * * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ @@ -37,9 +35,19 @@ namespace yii\db; class Query extends \yii\base\Component { /** - * @var string|array the columns being selected. This refers to the SELECT clause in a SQL - * statement. It can be either a string (e.g. `'id, name'`) or an array (e.g. `array('id', 'name')`). - * If not set, if means all columns. + * Sort ascending + * @see orderBy + */ + const SORT_ASC = false; + /** + * Sort ascending + * @see orderBy + */ + const SORT_DESC = true; + + /** + * @var array the columns being selected. For example, `array('id', 'name')`. + * This is used to construct the SELECT clause in a SQL statement. If not set, if means selecting all columns. * @see select() */ public $select; @@ -54,8 +62,8 @@ class Query extends \yii\base\Component */ public $distinct; /** - * @var string|array the table(s) to be selected from. This refers to the FROM clause in a SQL statement. - * It can be either a string (e.g. `'tbl_user, tbl_post'`) or an array (e.g. `array('tbl_user', 'tbl_post')`). + * @var array the table(s) to be selected from. For example, `array('tbl_user', 'tbl_post')`. + * This is used to construct the FROM clause in a SQL statement. * @see from() */ public $from; @@ -75,20 +83,33 @@ class Query extends \yii\base\Component */ public $offset; /** - * @var string|array how to sort the query results. This refers to the ORDER BY clause in a SQL statement. - * It can be either a string (e.g. `'id ASC, name DESC'`) or an array (e.g. `array('id ASC', 'name DESC')`). + * @var array how to sort the query results. This is used to construct the ORDER BY clause in a SQL statement. + * The array keys are the columns to be sorted by, and the array values are the corresponding sort directions which + * can be either [[Query::SORT_ASC]] or [[Query::SORT_DESC]]. The array may also contain [[Expression]] objects. + * If that is the case, the expressions will be converted into strings without any change. */ public $orderBy; /** - * @var string|array how to group the query results. This refers to the GROUP BY clause in a SQL statement. - * It can be either a string (e.g. `'company, department'`) or an array (e.g. `array('company', 'department')`). + * @var array how to group the query results. For example, `array('company', 'department')`. + * This is used to construct the GROUP BY clause in a SQL statement. */ public $groupBy; /** - * @var string|array how to join with other tables. This refers to the JOIN clause in a SQL statement. - * It can be either a string (e.g. `'LEFT JOIN tbl_user ON tbl_user.id=author_id'`) or an array (e.g. - * `array('LEFT JOIN tbl_user ON tbl_user.id=author_id', 'LEFT JOIN tbl_team ON tbl_team.id=team_id')`). - * @see join() + * @var array how to join with other tables. Each array element represents the specification + * of one join which has the following structure: + * + * ~~~ + * array($joinType, $tableName, $joinCondition) + * ~~~ + * + * For example, + * + * ~~~ + * array( + * array('INNER JOIN', 'tbl_user', 'tbl_user.id = author_id'), + * array('LEFT JOIN', 'tbl_team', 'tbl_team.id = team_id'), + * ) + * ~~~ */ public $join; /** @@ -97,9 +118,8 @@ class Query extends \yii\base\Component */ public $having; /** - * @var string|Query[] the UNION clause(s) in a SQL statement. This can be either a string - * representing a single UNION clause or an array representing multiple UNION clauses. - * Each union clause can be a string or a `Query` object which refers to the SQL statement. + * @var array this is used to construct the UNION clause(s) in a SQL statement. + * Each array element can be either a string or a [[Query]] object representing a sub-query. */ public $union; /** @@ -117,7 +137,7 @@ class Query extends \yii\base\Component public function createCommand($db = null) { if ($db === null) { - $db = \Yii::$application->db; + $db = \Yii::$app->db; } $sql = $db->getQueryBuilder()->build($this); return $db->createCommand($sql, $this->params); @@ -136,6 +156,9 @@ class Query extends \yii\base\Component */ public function select($columns, $option = null) { + if (!is_array($columns)) { + $columns = preg_split('/\s*,\s*/', trim($columns), -1, PREG_SPLIT_NO_EMPTY); + } $this->select = $columns; $this->selectOption = $option; return $this; @@ -163,6 +186,9 @@ class Query extends \yii\base\Component */ public function from($tables) { + if (!is_array($tables)) { + $tables = preg_split('/\s*,\s*/', trim($tables), -1, PREG_SPLIT_NO_EMPTY); + } $this->from = $tables; return $this; } @@ -362,10 +388,13 @@ class Query extends \yii\base\Component * The method will automatically quote the column names unless a column contains some parenthesis * (which means the column contains a DB expression). * @return Query the query object itself - * @see addGroup() + * @see addGroupBy() */ public function groupBy($columns) { + if (!is_array($columns)) { + $columns = preg_split('/\s*,\s*/', trim($columns), -1, PREG_SPLIT_NO_EMPTY); + } $this->groupBy = $columns; return $this; } @@ -377,19 +406,16 @@ class Query extends \yii\base\Component * The method will automatically quote the column names unless a column contains some parenthesis * (which means the column contains a DB expression). * @return Query the query object itself - * @see group() + * @see groupBy() */ - public function addGroup($columns) + public function addGroupBy($columns) { - if (empty($this->groupBy)) { + if (!is_array($columns)) { + $columns = preg_split('/\s*,\s*/', trim($columns), -1, PREG_SPLIT_NO_EMPTY); + } + if ($this->groupBy === null) { $this->groupBy = $columns; } else { - if (!is_array($this->groupBy)) { - $this->groupBy = preg_split('/\s*,\s*/', trim($this->groupBy), -1, PREG_SPLIT_NO_EMPTY); - } - if (!is_array($columns)) { - $columns = preg_split('/\s*,\s*/', trim($columns), -1, PREG_SPLIT_NO_EMPTY); - } $this->groupBy = array_merge($this->groupBy, $columns); } return $this; @@ -456,43 +482,58 @@ class Query extends \yii\base\Component /** * Sets the ORDER BY part of the query. * @param string|array $columns the columns (and the directions) to be ordered by. - * Columns can be specified in either a string (e.g. "id ASC, name DESC") or an array (e.g. array('id ASC', 'name DESC')). + * Columns can be specified in either a string (e.g. "id ASC, name DESC") or an array + * (e.g. `array('id' => Query::SORT_ASC ASC, 'name' => Query::SORT_DESC)`). * The method will automatically quote the column names unless a column contains some parenthesis * (which means the column contains a DB expression). * @return Query the query object itself - * @see addOrder() + * @see addOrderBy() */ public function orderBy($columns) { - $this->orderBy = $columns; + $this->orderBy = $this->normalizeOrderBy($columns); return $this; } /** * Adds additional ORDER BY columns to the query. * @param string|array $columns the columns (and the directions) to be ordered by. - * Columns can be specified in either a string (e.g. "id ASC, name DESC") or an array (e.g. array('id ASC', 'name DESC')). + * Columns can be specified in either a string (e.g. "id ASC, name DESC") or an array + * (e.g. `array('id' => Query::SORT_ASC ASC, 'name' => Query::SORT_DESC)`). * The method will automatically quote the column names unless a column contains some parenthesis * (which means the column contains a DB expression). * @return Query the query object itself - * @see order() + * @see orderBy() */ public function addOrderBy($columns) { - if (empty($this->orderBy)) { + $columns = $this->normalizeOrderBy($columns); + if ($this->orderBy === null) { $this->orderBy = $columns; } else { - if (!is_array($this->orderBy)) { - $this->orderBy = preg_split('/\s*,\s*/', trim($this->orderBy), -1, PREG_SPLIT_NO_EMPTY); - } - if (!is_array($columns)) { - $columns = preg_split('/\s*,\s*/', trim($columns), -1, PREG_SPLIT_NO_EMPTY); - } $this->orderBy = array_merge($this->orderBy, $columns); } return $this; } + protected function normalizeOrderBy($columns) + { + if (is_array($columns)) { + return $columns; + } else { + $columns = preg_split('/\s*,\s*/', trim($columns), -1, PREG_SPLIT_NO_EMPTY); + $result = array(); + foreach ($columns as $column) { + if (preg_match('/^(.*?)\s+(asc|desc)$/i', $column, $matches)) { + $result[$matches[1]] = strcasecmp($matches[2], 'desc') ? self::SORT_ASC : self::SORT_DESC; + } else { + $result[$column] = self::SORT_ASC; + } + } + return $result; + } + } + /** * Sets the LIMIT part of the query. * @param integer $limit the limit diff --git a/framework/db/QueryBuilder.php b/framework/db/QueryBuilder.php index 35bfcb3..75375cc 100644 --- a/framework/db/QueryBuilder.php +++ b/framework/db/QueryBuilder.php @@ -1,9 +1,7 @@ <?php /** - * QueryBuilder class file. - * * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ @@ -62,10 +60,10 @@ class QueryBuilder extends \yii\base\Object $this->buildFrom($query->from), $this->buildJoin($query->join), $this->buildWhere($query->where), - $this->buildGroup($query->groupBy), + $this->buildGroupBy($query->groupBy), $this->buildHaving($query->having), $this->buildUnion($query->union), - $this->buildOrder($query->orderBy), + $this->buildOrderBy($query->orderBy), $this->buildLimit($query->limit, $query->offset), ); return implode($this->separator, array_filter($clauses)); @@ -131,11 +129,10 @@ class QueryBuilder extends \yii\base\Object * @param string $table the table that new rows will be inserted into. * @param array $columns the column names * @param array $rows the rows to be batch inserted into the table - * @param array $params the parameters to be bound to the command * @return string the batch INSERT SQL statement * @throws NotSupportedException if this is not supported by the underlying DBMS */ - public function batchInsert($table, $columns, $rows, $params = array()) + public function batchInsert($table, $columns, $rows) { throw new NotSupportedException($this->db->getDriverName() . ' does not support batch insert.'); @@ -593,21 +590,19 @@ class QueryBuilder extends \yii\base\Object return $operator === 'IN' ? '0=1' : ''; } - if (is_array($column)) { - if (count($column) > 1) { - return $this->buildCompositeInCondition($operator, $column, $values); + if (count($column) > 1) { + return $this->buildCompositeInCondition($operator, $column, $values); + } elseif (is_array($column)) { + $column = reset($column); + } + foreach ($values as $i => $value) { + if (is_array($value)) { + $value = isset($value[$column]) ? $value[$column] : null; + } + if ($value === null) { + $values[$i] = 'NULL'; } else { - $column = reset($column); - foreach ($values as $i => $value) { - if (is_array($value)) { - $value = isset($value[$column]) ? $value[$column] : null; - } - if ($value === null) { - $values[$i] = 'NULL'; - } else { - $values[$i] = is_string($value) ? $this->db->quoteValue($value) : (string)$value; - } - } + $values[$i] = is_string($value) ? $this->db->quoteValue($value) : (string)$value; } } if (strpos($column, '(') === false) { @@ -678,7 +673,7 @@ class QueryBuilder extends \yii\base\Object } /** - * @param string|array $columns + * @param array $columns * @param boolean $distinct * @param string $selectOption * @return string the SELECT clause built from [[query]]. @@ -694,13 +689,6 @@ class QueryBuilder extends \yii\base\Object return $select . ' *'; } - if (!is_array($columns)) { - if (strpos($columns, '(') !== false) { - return $select . ' ' . $columns; - } else { - $columns = preg_split('/\s*,\s*/', trim($columns), -1, PREG_SPLIT_NO_EMPTY); - } - } foreach ($columns as $i => $column) { if (is_object($column)) { $columns[$i] = (string)$column; @@ -721,7 +709,7 @@ class QueryBuilder extends \yii\base\Object } /** - * @param string|array $tables + * @param array $tables * @return string the FROM clause built from [[query]]. */ public function buildFrom($tables) @@ -730,13 +718,6 @@ class QueryBuilder extends \yii\base\Object return ''; } - if (!is_array($tables)) { - if (strpos($tables, '(') !== false) { - return 'FROM ' . $tables; - } else { - $tables = preg_split('/\s*,\s*/', trim($tables), -1, PREG_SPLIT_NO_EMPTY); - } - } foreach ($tables as $i => $table) { if (strpos($table, '(') === false) { if (preg_match('/^(.*?)(?i:\s+as\s+|\s+)(.*)$/i', $table, $matches)) { // with alias @@ -757,37 +738,36 @@ class QueryBuilder extends \yii\base\Object /** * @param string|array $joins * @return string the JOIN clause built from [[query]]. + * @throws Exception if the $joins parameter is not in proper format */ public function buildJoin($joins) { if (empty($joins)) { return ''; } - if (is_string($joins)) { - return $joins; - } foreach ($joins as $i => $join) { - if (is_array($join)) { // 0:join type, 1:table name, 2:on-condition - if (isset($join[0], $join[1])) { - $table = $join[1]; - if (strpos($table, '(') === false) { - if (preg_match('/^(.*?)(?i:\s+as\s+|\s+)(.*)$/', $table, $matches)) { // with alias - $table = $this->db->quoteTableName($matches[1]) . ' ' . $this->db->quoteTableName($matches[2]); - } else { - $table = $this->db->quoteTableName($table); - } + if (is_object($join)) { + $joins[$i] = (string)$join; + } elseif (is_array($join) && isset($join[0], $join[1])) { + // 0:join type, 1:table name, 2:on-condition + $table = $join[1]; + if (strpos($table, '(') === false) { + if (preg_match('/^(.*?)(?i:\s+as\s+|\s+)(.*)$/', $table, $matches)) { // with alias + $table = $this->db->quoteTableName($matches[1]) . ' ' . $this->db->quoteTableName($matches[2]); + } else { + $table = $this->db->quoteTableName($table); } - $joins[$i] = $join[0] . ' ' . $table; - if (isset($join[2])) { - $condition = $this->buildCondition($join[2]); - if ($condition !== '') { - $joins[$i] .= ' ON ' . $this->buildCondition($join[2]); - } + } + $joins[$i] = $join[0] . ' ' . $table; + if (isset($join[2])) { + $condition = $this->buildCondition($join[2]); + if ($condition !== '') { + $joins[$i] .= ' ON ' . $this->buildCondition($join[2]); } - } else { - throw new Exception('A join clause must be specified as an array of at least two elements.'); } + } else { + throw new Exception('A join clause must be specified as an array of join type, join table, and optionally join condition.'); } } @@ -805,16 +785,12 @@ class QueryBuilder extends \yii\base\Object } /** - * @param string|array $columns + * @param array $columns * @return string the GROUP BY clause */ - public function buildGroup($columns) + public function buildGroupBy($columns) { - if (empty($columns)) { - return ''; - } else { - return 'GROUP BY ' . $this->buildColumns($columns); - } + return empty($columns) ? '' : 'GROUP BY ' . $this->buildColumns($columns); } /** @@ -828,36 +804,24 @@ class QueryBuilder extends \yii\base\Object } /** - * @param string|array $columns + * @param array $columns * @return string the ORDER BY clause built from [[query]]. */ - public function buildOrder($columns) + public function buildOrderBy($columns) { if (empty($columns)) { return ''; } - if (!is_array($columns)) { - if (strpos($columns, '(') !== false) { - return 'ORDER BY ' . $columns; + $orders = array(); + foreach ($columns as $name => $direction) { + if (is_object($direction)) { + $orders[] = (string)$direction; } else { - $columns = preg_split('/\s*,\s*/', trim($columns), -1, PREG_SPLIT_NO_EMPTY); - } - } - foreach ($columns as $i => $column) { - if (is_object($column)) { - $columns[$i] = (string)$column; - } elseif (strpos($column, '(') === false) { - if (preg_match('/^(.*?)\s+(asc|desc)$/i', $column, $matches)) { - $columns[$i] = $this->db->quoteColumnName($matches[1]) . ' ' . $matches[2]; - } else { - $columns[$i] = $this->db->quoteColumnName($column); - } + $orders[] = $this->db->quoteColumnName($name) . ($direction === Query::SORT_DESC ? ' DESC' : ''); } } - if (is_array($columns)) { - $columns = implode(', ', $columns); - } - return 'ORDER BY ' . $columns; + + return 'ORDER BY ' . implode(', ', $orders); } /** @@ -878,7 +842,7 @@ class QueryBuilder extends \yii\base\Object } /** - * @param string|array $unions + * @param array $unions * @return string the UNION clause built from [[query]]. */ public function buildUnion($unions) @@ -886,9 +850,6 @@ class QueryBuilder extends \yii\base\Object if (empty($unions)) { return ''; } - if (!is_array($unions)) { - $unions = array($unions); - } foreach ($unions as $i => $union) { if ($union instanceof Query) { $unions[$i] = $this->build($union); diff --git a/framework/db/Schema.php b/framework/db/Schema.php index 7415bee..71bc9a2 100644 --- a/framework/db/Schema.php +++ b/framework/db/Schema.php @@ -1,14 +1,13 @@ <?php /** - * Driver class file. - * * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ namespace yii\db; +use Yii; use yii\base\NotSupportedException; use yii\base\InvalidCallException; use yii\caching\Cache; @@ -86,21 +85,21 @@ abstract class Schema extends \yii\base\Object $db = $this->db; $realName = $this->getRealTableName($name); - /** @var $cache Cache */ - if ($db->enableSchemaCache && ($cache = \Yii::$application->getComponent($db->schemaCacheID)) !== null && !in_array($name, $db->schemaCacheExclude, true)) { - $key = $this->getCacheKey($cache, $name); - if ($refresh || ($table = $cache->get($key)) === false) { - $table = $this->loadTableSchema($realName); - if ($table !== null) { - $cache->set($key, $table, $db->schemaCacheDuration); + if ($db->enableSchemaCache && !in_array($name, $db->schemaCacheExclude, true)) { + /** @var $cache Cache */ + $cache = is_string($db->schemaCache) ? Yii::$app->getComponent($db->schemaCache) : $db->schemaCache; + if ($cache instanceof Cache) { + $key = $this->getCacheKey($cache, $name); + if ($refresh || ($table = $cache->get($key)) === false) { + $table = $this->loadTableSchema($realName); + if ($table !== null) { + $cache->set($key, $table, $db->schemaCacheDuration); + } } + return $this->_tables[$name] = $table; } - $this->_tables[$name] = $table; - } else { - $this->_tables[$name] = $table = $this->loadTableSchema($realName); } - - return $table; + return $this->_tables[$name] = $table = $this->loadTableSchema($realName); } /** @@ -111,7 +110,12 @@ abstract class Schema extends \yii\base\Object */ public function getCacheKey($cache, $name) { - return $cache->buildKey(__CLASS__, $this->db->dsn, $this->db->username, $name); + return $cache->buildKey(array( + __CLASS__, + $this->db->dsn, + $this->db->username, + $name, + )); } /** @@ -170,8 +174,9 @@ abstract class Schema extends \yii\base\Object */ public function refresh() { - /** @var $cache \yii\caching\Cache */ - if ($this->db->enableSchemaCache && ($cache = \Yii::$application->getComponent($this->db->schemaCacheID)) !== null) { + /** @var $cache Cache */ + $cache = is_string($this->db->schemaCache) ? Yii::$app->getComponent($this->db->schemaCache) : $this->db->schemaCache; + if ($this->db->enableSchemaCache && $cache instanceof Cache) { foreach ($this->_tables as $name => $table) { $cache->delete($this->getCacheKey($cache, $name)); } diff --git a/framework/db/TableSchema.php b/framework/db/TableSchema.php index 987d221..1065b51 100644 --- a/framework/db/TableSchema.php +++ b/framework/db/TableSchema.php @@ -1,7 +1,5 @@ <?php /** - * TableSchema class file. - * * @link http://www.yiiframework.com/ * @copyright Copyright © 2008-2011 Yii Software LLC * @license http://www.yiiframework.com/license/ @@ -9,7 +7,7 @@ namespace yii\db; -use yii\base\InvalidCallException; +use yii\base\InvalidParamException; /** * TableSchema represents the metadata of a database table. @@ -83,7 +81,7 @@ class TableSchema extends \yii\base\Object /** * Manually specifies the primary key for this table. * @param string|array $keys the primary key (can be composite) - * @throws InvalidCallException if the specified key cannot be found in the table. + * @throws InvalidParamException if the specified key cannot be found in the table. */ public function fixPrimaryKey($keys) { @@ -98,7 +96,7 @@ class TableSchema extends \yii\base\Object if (isset($this->columns[$key])) { $this->columns[$key]->isPrimaryKey = true; } else { - throw new InvalidCallException("Primary key '$key' cannot be found in table '{$this->name}'."); + throw new InvalidParamException("Primary key '$key' cannot be found in table '{$this->name}'."); } } } diff --git a/framework/db/Transaction.php b/framework/db/Transaction.php index 3e53c0c..177d2cb 100644 --- a/framework/db/Transaction.php +++ b/framework/db/Transaction.php @@ -1,9 +1,7 @@ <?php /** - * Transaction class file. - * * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ diff --git a/framework/db/mysql/QueryBuilder.php b/framework/db/mysql/QueryBuilder.php index 6168409..a078b9a 100644 --- a/framework/db/mysql/QueryBuilder.php +++ b/framework/db/mysql/QueryBuilder.php @@ -1,16 +1,14 @@ <?php /** - * QueryBuilder class file. - * * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ namespace yii\db\mysql; use yii\db\Exception; -use yii\base\InvalidCallException; +use yii\base\InvalidParamException; /** * QueryBuilder is the query builder for MySQL databases. @@ -54,7 +52,7 @@ class QueryBuilder extends \yii\db\QueryBuilder $quotedTable = $this->db->quoteTableName($table); $row = $this->db->createCommand('SHOW CREATE TABLE ' . $quotedTable)->queryRow(); if ($row === false) { - throw new Exception("Unable to find '$oldName' in table '$table'."); + throw new Exception("Unable to find column '$oldName' in table '$table'."); } if (isset($row['Create Table'])) { $sql = $row['Create Table']; @@ -98,7 +96,7 @@ class QueryBuilder extends \yii\db\QueryBuilder * @param mixed $value the value for the primary key of the next new row inserted. If this is not set, * the next new row's primary key will have a value 1. * @return string the SQL statement for resetting sequence - * @throws InvalidCallException if the table does not exist or there is no sequence associated with the table. + * @throws InvalidParamException if the table does not exist or there is no sequence associated with the table. */ public function resetSequence($tableName, $value = null) { @@ -113,9 +111,9 @@ class QueryBuilder extends \yii\db\QueryBuilder } return "ALTER TABLE $tableName AUTO_INCREMENT=$value"; } elseif ($table === null) { - throw new InvalidCallException("Table not found: $tableName"); + throw new InvalidParamException("Table not found: $tableName"); } else { - throw new InvalidCallException("There is not sequence associated with table '$tableName'.'"); + throw new InvalidParamException("There is not sequence associated with table '$tableName'.'"); } } diff --git a/framework/db/mysql/Schema.php b/framework/db/mysql/Schema.php index 32df0b3..501149a 100644 --- a/framework/db/mysql/Schema.php +++ b/framework/db/mysql/Schema.php @@ -1,9 +1,7 @@ <?php /** - * Schema class file. - * * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ diff --git a/framework/db/sqlite/QueryBuilder.php b/framework/db/sqlite/QueryBuilder.php index b9b8f5e..3aa89e7 100644 --- a/framework/db/sqlite/QueryBuilder.php +++ b/framework/db/sqlite/QueryBuilder.php @@ -1,17 +1,15 @@ <?php /** - * QueryBuilder class file. - * * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ namespace yii\db\sqlite; use yii\db\Exception; +use yii\base\InvalidParamException; use yii\base\NotSupportedException; -use yii\base\InvalidCallException; /** * QueryBuilder is the query builder for SQLite databases. @@ -50,7 +48,7 @@ class QueryBuilder extends \yii\db\QueryBuilder * @param mixed $value the value for the primary key of the next new row inserted. If this is not set, * the next new row's primary key will have a value 1. * @return string the SQL statement for resetting sequence - * @throws InvalidCallException if the table does not exist or there is no sequence associated with the table. + * @throws InvalidParamException if the table does not exist or there is no sequence associated with the table. */ public function resetSequence($tableName, $value = null) { @@ -70,9 +68,9 @@ class QueryBuilder extends \yii\db\QueryBuilder } catch (Exception $e) { } } elseif ($table === null) { - throw new InvalidCallException("Table not found: $tableName"); + throw new InvalidParamException("Table not found: $tableName"); } else { - throw new InvalidCallException("There is not sequence associated with table '$tableName'.'"); + throw new InvalidParamException("There is not sequence associated with table '$tableName'.'"); } } diff --git a/framework/db/sqlite/Schema.php b/framework/db/sqlite/Schema.php index 8f7cb08..45f8392 100644 --- a/framework/db/sqlite/Schema.php +++ b/framework/db/sqlite/Schema.php @@ -1,9 +1,7 @@ <?php /** - * Schema class file. - * * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ diff --git a/framework/util/ArrayHelper.php b/framework/helpers/ArrayHelper.php similarity index 72% rename from framework/util/ArrayHelper.php rename to framework/helpers/ArrayHelper.php index c14121b..65fa962 100644 --- a/framework/util/ArrayHelper.php +++ b/framework/helpers/ArrayHelper.php @@ -1,15 +1,14 @@ <?php /** - * ArrayHelper class file. - * + * @copyright Copyright (c) 2008 Yii Software LLC * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ -namespace yii\util; +namespace yii\helpers; -use yii\base\InvalidCallException; +use Yii; +use yii\base\InvalidParamException; /** * ArrayHelper provides additional array functionality you can use in your @@ -60,11 +59,11 @@ class ArrayHelper * * ~~~ * // working with array - * $username = \yii\util\ArrayHelper::getValue($_POST, 'username'); + * $username = \yii\helpers\ArrayHelper::getValue($_POST, 'username'); * // working with object - * $username = \yii\util\ArrayHelper::getValue($user, 'username'); + * $username = \yii\helpers\ArrayHelper::getValue($user, 'username'); * // working with anonymous function - * $fullName = \yii\util\ArrayHelper::getValue($user, function($user, $defaultValue) { + * $fullName = \yii\helpers\ArrayHelper::getValue($user, function($user, $defaultValue) { * return $user->firstName . ' ' . $user->lastName; * }); * ~~~ @@ -242,7 +241,7 @@ class ArrayHelper * value is for sorting strings in case-insensitive manner. Please refer to * See [PHP manual](http://php.net/manual/en/function.sort.php) for more details. * When sorting by multiple keys with different sort flags, use an array of sort flags. - * @throws InvalidCallException if the $ascending or $sortFlag parameters do not have + * @throws InvalidParamException if the $ascending or $sortFlag parameters do not have * correct number of elements as that of $key. */ public static function multisort(&$array, $key, $ascending = true, $sortFlag = SORT_REGULAR) @@ -255,12 +254,12 @@ class ArrayHelper if (is_scalar($ascending)) { $ascending = array_fill(0, $n, $ascending); } elseif (count($ascending) !== $n) { - throw new InvalidCallException('The length of $ascending parameter must be the same as that of $keys.'); + throw new InvalidParamException('The length of $ascending parameter must be the same as that of $keys.'); } if (is_scalar($sortFlag)) { $sortFlag = array_fill(0, $n, $sortFlag); } elseif (count($sortFlag) !== $n) { - throw new InvalidCallException('The length of $ascending parameter must be the same as that of $keys.'); + throw new InvalidParamException('The length of $ascending parameter must be the same as that of $keys.'); } $args = array(); foreach ($keys as $i => $key) { @@ -281,4 +280,61 @@ class ArrayHelper $args[] = &$array; call_user_func_array('array_multisort', $args); } + + /** + * Encodes special characters in an array of strings into HTML entities. + * Both the array keys and values will be encoded. + * If a value is an array, this method will also encode it recursively. + * @param array $data data to be encoded + * @param boolean $valuesOnly whether to encode array values only. If false, + * both the array keys and array values will be encoded. + * @param string $charset the charset that the data is using. If not set, + * [[\yii\base\Application::charset]] will be used. + * @return array the encoded data + * @see http://www.php.net/manual/en/function.htmlspecialchars.php + */ + public static function htmlEncode($data, $valuesOnly = true, $charset = null) + { + if ($charset === null) { + $charset = Yii::$app->charset; + } + $d = array(); + foreach ($data as $key => $value) { + if (!$valuesOnly && is_string($key)) { + $key = htmlspecialchars($key, ENT_QUOTES, $charset); + } + if (is_string($value)) { + $d[$key] = htmlspecialchars($value, ENT_QUOTES, $charset); + } elseif (is_array($value)) { + $d[$key] = static::htmlEncode($value, $charset); + } + } + return $d; + } + + /** + * Decodes HTML entities into the corresponding characters in an array of strings. + * Both the array keys and values will be decoded. + * If a value is an array, this method will also decode it recursively. + * @param array $data data to be decoded + * @param boolean $valuesOnly whether to decode array values only. If false, + * both the array keys and array values will be decoded. + * @return array the decoded data + * @see http://www.php.net/manual/en/function.htmlspecialchars-decode.php + */ + public static function htmlDecode($data, $valuesOnly = true) + { + $d = array(); + foreach ($data as $key => $value) { + if (!$valuesOnly && is_string($key)) { + $key = htmlspecialchars_decode($key, ENT_QUOTES); + } + if (is_string($value)) { + $d[$key] = htmlspecialchars_decode($value, ENT_QUOTES); + } elseif (is_array($value)) { + $d[$key] = static::htmlDecode($value); + } + } + return $d; + } } \ No newline at end of file diff --git a/framework/util/ConsoleColor.php b/framework/helpers/ConsoleColor.php similarity index 92% rename from framework/util/ConsoleColor.php rename to framework/helpers/ConsoleColor.php index 1fadc40..429aeb1 100644 --- a/framework/util/ConsoleColor.php +++ b/framework/helpers/ConsoleColor.php @@ -1,23 +1,13 @@ <?php /** - * ConsoleColor class file. - * * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ -namespace yii\console; +namespace yii\helpers; -// todo define how subclassing will work -// todo add a run() or render() method // todo test this on all kinds of terminals, especially windows (check out lib ncurses) -// todo not sure if all methods should be static - -// todo subclass DetailView -// todo subclass GridView -// todo more subclasses - /** * Console View is the base class for console view components @@ -359,7 +349,7 @@ class ConsoleColor } $styleString[] = array(); foreach($styleA as $name => $content) { - if ($name = 'text-decoration') { + if ($name === 'text-decoration') { $content = implode(' ', $content); } $styleString[] = $name.':'.$content; diff --git a/framework/util/FileHelper.php b/framework/helpers/FileHelper.php similarity index 97% rename from framework/util/FileHelper.php rename to framework/helpers/FileHelper.php index c65e4f0..f850b98 100644 --- a/framework/util/FileHelper.php +++ b/framework/helpers/FileHelper.php @@ -3,11 +3,11 @@ * Filesystem helper class file. * * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ -namespace yii\util; +namespace yii\helpers; use yii\base\Exception; use yii\base\InvalidConfigException; @@ -43,7 +43,7 @@ class FileHelper public static function ensureDirectory($path) { $p = \Yii::getAlias($path); - if ($p !== false && ($p = realpath($p)) !== false && is_dir($p)) { + if (($p = realpath($p)) !== false && is_dir($p)) { return $p; } else { throw new InvalidConfigException('Directory does not exist: ' . $path); @@ -91,10 +91,10 @@ class FileHelper public static function localize($file, $language = null, $sourceLanguage = null) { if ($language === null) { - $language = \Yii::$application->getLanguage(); + $language = \Yii::$app->language; } if ($sourceLanguage === null) { - $sourceLanguage = \Yii::$application->sourceLanguage; + $sourceLanguage = \Yii::$app->sourceLanguage; } if ($language === $sourceLanguage) { return $file; diff --git a/framework/helpers/Html.php b/framework/helpers/Html.php new file mode 100644 index 0000000..b2ca576 --- /dev/null +++ b/framework/helpers/Html.php @@ -0,0 +1,981 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\helpers; + +use Yii; +use yii\base\InvalidParamException; + +/** + * Html provides a set of static methods for generating commonly used HTML tags. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class Html +{ + /** + * @var boolean whether to close void (empty) elements. Defaults to true. + * @see voidElements + */ + public static $closeVoidElements = true; + /** + * @var array list of void elements (element name => 1) + * @see closeVoidElements + * @see http://www.w3.org/TR/html-markup/syntax.html#void-element + */ + public static $voidElements = array( + 'area' => 1, + 'base' => 1, + 'br' => 1, + 'col' => 1, + 'command' => 1, + 'embed' => 1, + 'hr' => 1, + 'img' => 1, + 'input' => 1, + 'keygen' => 1, + 'link' => 1, + 'meta' => 1, + 'param' => 1, + 'source' => 1, + 'track' => 1, + 'wbr' => 1, + ); + /** + * @var boolean whether to show the values of boolean attributes in element tags. + * If false, only the attribute names will be generated. + * @see booleanAttributes + */ + public static $showBooleanAttributeValues = true; + /** + * @var array list of boolean attributes. The presence of a boolean attribute on + * an element represents the true value, and the absence of the attribute represents the false value. + * @see showBooleanAttributeValues + * @see http://www.w3.org/TR/html5/infrastructure.html#boolean-attributes + */ + public static $booleanAttributes = array( + 'async' => 1, + 'autofocus' => 1, + 'autoplay' => 1, + 'checked' => 1, + 'controls' => 1, + 'declare' => 1, + 'default' => 1, + 'defer' => 1, + 'disabled' => 1, + 'formnovalidate' => 1, + 'hidden' => 1, + 'ismap' => 1, + 'loop' => 1, + 'multiple' => 1, + 'muted' => 1, + 'nohref' => 1, + 'noresize' => 1, + 'novalidate' => 1, + 'open' => 1, + 'readonly' => 1, + 'required' => 1, + 'reversed' => 1, + 'scoped' => 1, + 'seamless' => 1, + 'selected' => 1, + 'typemustmatch' => 1, + ); + /** + * @var array the preferred order of attributes in a tag. This mainly affects the order of the attributes + * that are rendered by [[renderAttributes()]]. + */ + public static $attributeOrder = array( + 'type', + 'id', + 'class', + 'name', + 'value', + + 'href', + 'src', + 'action', + 'method', + + 'selected', + 'checked', + 'readonly', + 'disabled', + 'multiple', + + 'size', + 'maxlength', + 'width', + 'height', + 'rows', + 'cols', + + 'alt', + 'title', + 'rel', + 'media', + ); + + /** + * Encodes special characters into HTML entities. + * The [[yii\base\Application::charset|application charset]] will be used for encoding. + * @param string $content the content to be encoded + * @return string the encoded content + * @see decode + * @see http://www.php.net/manual/en/function.htmlspecialchars.php + */ + public static function encode($content) + { + return htmlspecialchars($content, ENT_QUOTES, Yii::$app->charset); + } + + /** + * Decodes special HTML entities back to the corresponding characters. + * This is the opposite of [[encode()]]. + * @param string $content the content to be decoded + * @return string the decoded content + * @see encode + * @see http://www.php.net/manual/en/function.htmlspecialchars-decode.php + */ + public static function decode($content) + { + return htmlspecialchars_decode($content, ENT_QUOTES); + } + + /** + * Generates a complete HTML tag. + * @param string $name the tag name + * @param string $content the content to be enclosed between the start and end tags. It will not be HTML-encoded. + * If this is coming from end users, you should consider [[encode()]] it to prevent XSS attacks. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated HTML tag + * @see beginTag + * @see endTag + */ + public static function tag($name, $content = '', $options = array()) + { + $html = '<' . $name . static::renderTagAttributes($options); + if (isset(static::$voidElements[strtolower($name)])) { + return $html . (static::$closeVoidElements ? ' />' : '>'); + } else { + return $html . ">$content</$name>"; + } + } + + /** + * Generates a start tag. + * @param string $name the tag name + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated start tag + * @see endTag + * @see tag + */ + public static function beginTag($name, $options = array()) + { + return '<' . $name . static::renderTagAttributes($options) . '>'; + } + + /** + * Generates an end tag. + * @param string $name the tag name + * @return string the generated end tag + * @see beginTag + * @see tag + */ + public static function endTag($name) + { + return "</$name>"; + } + + /** + * Encloses the given content within a CDATA tag. + * @param string $content the content to be enclosed within the CDATA tag + * @return string the CDATA tag with the enclosed content. + */ + public static function cdata($content) + { + return '<![CDATA[' . $content . ']]>'; + } + + /** + * Generates a style tag. + * @param string $content the style content + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * If the options does not contain "type", a "type" attribute with value "text/css" will be used. + * @return string the generated style tag + */ + public static function style($content, $options = array()) + { + if (!isset($options['type'])) { + $options['type'] = 'text/css'; + } + return static::tag('style', "/*<![CDATA[*/\n{$content}\n/*]]>*/", $options); + } + + /** + * Generates a script tag. + * @param string $content the script content + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * If the options does not contain "type", a "type" attribute with value "text/javascript" will be rendered. + * @return string the generated script tag + */ + public static function script($content, $options = array()) + { + if (!isset($options['type'])) { + $options['type'] = 'text/javascript'; + } + return static::tag('script', "/*<![CDATA[*/\n{$content}\n/*]]>*/", $options); + } + + /** + * Generates a link tag that refers to an external CSS file. + * @param array|string $url the URL of the external CSS file. This parameter will be processed by [[url()]]. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated link tag + * @see url + */ + public static function cssFile($url, $options = array()) + { + $options['rel'] = 'stylesheet'; + $options['type'] = 'text/css'; + $options['href'] = static::url($url); + return static::tag('link', '', $options); + } + + /** + * Generates a script tag that refers to an external JavaScript file. + * @param string $url the URL of the external JavaScript file. This parameter will be processed by [[url()]]. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated script tag + * @see url + */ + public static function jsFile($url, $options = array()) + { + $options['type'] = 'text/javascript'; + $options['src'] = static::url($url); + return static::tag('script', '', $options); + } + + /** + * Generates a form start tag. + * @param array|string $action the form action URL. This parameter will be processed by [[url()]]. + * @param string $method the form submission method, either "post" or "get" (case-insensitive) + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated form start tag. + * @see endForm + */ + public static function beginForm($action = '', $method = 'post', $options = array()) + { + $action = static::url($action); + + // query parameters in the action are ignored for GET method + // we use hidden fields to add them back + $hiddens = array(); + if (!strcasecmp($method, 'get') && ($pos = strpos($action, '?')) !== false) { + foreach (explode('&', substr($action, $pos + 1)) as $pair) { + if (($pos1 = strpos($pair, '=')) !== false) { + $hiddens[] = static::hiddenInput(urldecode(substr($pair, 0, $pos1)), urldecode(substr($pair, $pos1 + 1))); + } else { + $hiddens[] = static::hiddenInput(urldecode($pair), ''); + } + } + $action = substr($action, 0, $pos); + } + + $options['action'] = $action; + $options['method'] = $method; + $form = static::beginTag('form', $options); + if ($hiddens !== array()) { + $form .= "\n" . implode("\n", $hiddens); + } + + return $form; + } + + /** + * Generates a form end tag. + * @return string the generated tag + * @see beginForm + */ + public static function endForm() + { + return '</form>'; + } + + /** + * Generates a hyperlink tag. + * @param string $text link body. It will NOT be HTML-encoded. Therefore you can pass in HTML code + * such as an image tag. If this is is coming from end users, you should consider [[encode()]] + * it to prevent XSS attacks. + * @param array|string|null $url the URL for the hyperlink tag. This parameter will be processed by [[url()]] + * and will be used for the "href" attribute of the tag. If this parameter is null, the "href" attribute + * will not be generated. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated hyperlink + * @see url + */ + public static function a($text, $url = null, $options = array()) + { + if ($url !== null) { + $options['href'] = static::url($url); + } + return static::tag('a', $text, $options); + } + + /** + * Generates a mailto hyperlink. + * @param string $text link body. It will NOT be HTML-encoded. Therefore you can pass in HTML code + * such as an image tag. If this is is coming from end users, you should consider [[encode()]] + * it to prevent XSS attacks. + * @param string $email email address. If this is null, the first parameter (link body) will be treated + * as the email address and used. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated mailto link + */ + public static function mailto($text, $email = null, $options = array()) + { + return static::a($text, 'mailto:' . ($email === null ? $text : $email), $options); + } + + /** + * Generates an image tag. + * @param string $src the image URL. This parameter will be processed by [[url()]]. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated image tag + */ + public static function img($src, $options = array()) + { + $options['src'] = static::url($src); + if (!isset($options['alt'])) { + $options['alt'] = ''; + } + return static::tag('img', null, $options); + } + + /** + * Generates a label tag. + * @param string $content label text. It will NOT be HTML-encoded. Therefore you can pass in HTML code + * such as an image tag. If this is is coming from end users, you should consider [[encode()]] + * it to prevent XSS attacks. + * @param string $for the ID of the HTML element that this label is associated with. + * If this is null, the "for" attribute will not be generated. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated label tag + */ + public static function label($content, $for = null, $options = array()) + { + $options['for'] = $for; + return static::tag('label', $content, $options); + } + + /** + * Generates a button tag. + * @param string $name the name attribute. If it is null, the name attribute will not be generated. + * @param string $value the value attribute. If it is null, the value attribute will not be generated. + * @param string $content the content enclosed within the button tag. It will NOT be HTML-encoded. + * Therefore you can pass in HTML code such as an image tag. If this is is coming from end users, + * you should consider [[encode()]] it to prevent XSS attacks. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * If the options does not contain "type", a "type" attribute with value "button" will be rendered. + * @return string the generated button tag + */ + public static function button($name = null, $value = null, $content = 'Button', $options = array()) + { + $options['name'] = $name; + $options['value'] = $value; + if (!isset($options['type'])) { + $options['type'] = 'button'; + } + return static::tag('button', $content, $options); + } + + /** + * Generates a submit button tag. + * @param string $name the name attribute. If it is null, the name attribute will not be generated. + * @param string $value the value attribute. If it is null, the value attribute will not be generated. + * @param string $content the content enclosed within the button tag. It will NOT be HTML-encoded. + * Therefore you can pass in HTML code such as an image tag. If this is is coming from end users, + * you should consider [[encode()]] it to prevent XSS attacks. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated submit button tag + */ + public static function submitButton($name = null, $value = null, $content = 'Submit', $options = array()) + { + $options['type'] = 'submit'; + return static::button($name, $value, $content, $options); + } + + /** + * Generates a reset button tag. + * @param string $name the name attribute. If it is null, the name attribute will not be generated. + * @param string $value the value attribute. If it is null, the value attribute will not be generated. + * @param string $content the content enclosed within the button tag. It will NOT be HTML-encoded. + * Therefore you can pass in HTML code such as an image tag. If this is is coming from end users, + * you should consider [[encode()]] it to prevent XSS attacks. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated reset button tag + */ + public static function resetButton($name = null, $value = null, $content = 'Reset', $options = array()) + { + $options['type'] = 'reset'; + return static::button($name, $value, $content, $options); + } + + /** + * Generates an input type of the given type. + * @param string $type the type attribute. + * @param string $name the name attribute. If it is null, the name attribute will not be generated. + * @param string $value the value attribute. If it is null, the value attribute will not be generated. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated input tag + */ + public static function input($type, $name = null, $value = null, $options = array()) + { + $options['type'] = $type; + $options['name'] = $name; + $options['value'] = $value; + return static::tag('input', null, $options); + } + + /** + * Generates an input button. + * @param string $name the name attribute. + * @param string $value the value attribute. If it is null, the value attribute will not be generated. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated button tag + */ + public static function buttonInput($name, $value = 'Button', $options = array()) + { + return static::input('button', $name, $value, $options); + } + + /** + * Generates a submit input button. + * @param string $name the name attribute. If it is null, the name attribute will not be generated. + * @param string $value the value attribute. If it is null, the value attribute will not be generated. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated button tag + */ + public static function submitInput($name = null, $value = 'Submit', $options = array()) + { + return static::input('submit', $name, $value, $options); + } + + /** + * Generates a reset input button. + * @param string $name the name attribute. If it is null, the name attribute will not be generated. + * @param string $value the value attribute. If it is null, the value attribute will not be generated. + * @param array $options the attributes of the button tag. The values will be HTML-encoded using [[encode()]]. + * Attributes whose value is null will be ignored and not put in the tag returned. + * @return string the generated button tag + */ + public static function resetInput($name = null, $value = 'Reset', $options = array()) + { + return static::input('reset', $name, $value, $options); + } + + /** + * Generates a text input field. + * @param string $name the name attribute. + * @param string $value the value attribute. If it is null, the value attribute will not be generated. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated button tag + */ + public static function textInput($name, $value = null, $options = array()) + { + return static::input('text', $name, $value, $options); + } + + /** + * Generates a hidden input field. + * @param string $name the name attribute. + * @param string $value the value attribute. If it is null, the value attribute will not be generated. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated button tag + */ + public static function hiddenInput($name, $value = null, $options = array()) + { + return static::input('hidden', $name, $value, $options); + } + + /** + * Generates a password input field. + * @param string $name the name attribute. + * @param string $value the value attribute. If it is null, the value attribute will not be generated. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated button tag + */ + public static function passwordInput($name, $value = null, $options = array()) + { + return static::input('password', $name, $value, $options); + } + + /** + * Generates a file input field. + * To use a file input field, you should set the enclosing form's "enctype" attribute to + * be "multipart/form-data". After the form is submitted, the uploaded file information + * can be obtained via $_FILES[$name] (see PHP documentation). + * @param string $name the name attribute. + * @param string $value the value attribute. If it is null, the value attribute will not be generated. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated button tag + */ + public static function fileInput($name, $value = null, $options = array()) + { + return static::input('file', $name, $value, $options); + } + + /** + * Generates a text area input. + * @param string $name the input name + * @param string $value the input value. Note that it will be encoded using [[encode()]]. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated text area tag + */ + public static function textarea($name, $value = '', $options = array()) + { + $options['name'] = $name; + return static::tag('textarea', static::encode($value), $options); + } + + /** + * Generates a radio button input. + * @param string $name the name attribute. + * @param boolean $checked whether the radio button should be checked. + * @param string $value the value attribute. If it is null, the value attribute will not be rendered. + * @param array $options the tag options in terms of name-value pairs. The following options are supported: + * + * - uncheck: string, the value associated with the uncheck state of the radio button. When this attribute + * is present, a hidden input will be generated so that if the radio button is not checked and is submitted, + * the value of this attribute will still be submitted to the server via the hidden input. + * + * The rest of the options will be rendered as the attributes of the resulting tag. The values will + * be HTML-encoded using [[encode()]]. If a value is null, the corresponding attribute will not be rendered. + * + * @return string the generated radio button tag + */ + public static function radio($name, $checked = false, $value = '1', $options = array()) + { + $options['checked'] = $checked; + $options['value'] = $value; + if (isset($options['uncheck'])) { + // add a hidden field so that if the radio button is not selected, it still submits a value + $hidden = static::hiddenInput($name, $options['uncheck']); + unset($options['uncheck']); + } else { + $hidden = ''; + } + return $hidden . static::input('radio', $name, $value, $options); + } + + /** + * Generates a checkbox input. + * @param string $name the name attribute. + * @param boolean $checked whether the checkbox should be checked. + * @param string $value the value attribute. If it is null, the value attribute will not be rendered. + * @param array $options the tag options in terms of name-value pairs. The following options are supported: + * + * - uncheck: string, the value associated with the uncheck state of the checkbox. When this attribute + * is present, a hidden input will be generated so that if the checkbox is not checked and is submitted, + * the value of this attribute will still be submitted to the server via the hidden input. + * + * The rest of the options will be rendered as the attributes of the resulting tag. The values will + * be HTML-encoded using [[encode()]]. If a value is null, the corresponding attribute will not be rendered. + * + * @return string the generated checkbox tag + */ + public static function checkbox($name, $checked = false, $value = '1', $options = array()) + { + $options['checked'] = $checked; + $options['value'] = $value; + if (isset($options['uncheck'])) { + // add a hidden field so that if the checkbox is not selected, it still submits a value + $hidden = static::hiddenInput($name, $options['uncheck']); + unset($options['uncheck']); + } else { + $hidden = ''; + } + return $hidden . static::input('checkbox', $name, $value, $options); + } + + /** + * Generates a drop-down list. + * @param string $name the input name + * @param string $selection the selected value + * @param array $items the option data items. The array keys are option values, and the array values + * are the corresponding option labels. The array can also be nested (i.e. some array values are arrays too). + * For each sub-array, an option group will be generated whose label is the key associated with the sub-array. + * If you have a list of data models, you may convert them into the format described above using + * [[\yii\helpers\ArrayHelper::map()]]. + * + * Note, the values and labels will be automatically HTML-encoded by this method, and the blank spaces in + * the labels will also be HTML-encoded. + * @param array $options the tag options in terms of name-value pairs. The following options are supported: + * + * - prompt: string, a prompt text to be displayed as the first option; + * - options: array, the attributes for the select option tags. The array keys must be valid option values, + * and the array values are the extra attributes for the corresponding option tags. For example, + * + * ~~~ + * array( + * 'value1' => array('disabled' => true), + * 'value2' => array('label' => 'value 2'), + * ); + * ~~~ + * + * - groups: array, the attributes for the optgroup tags. The structure of this is similar to that of 'options', + * except that the array keys represent the optgroup labels specified in $items. + * + * The rest of the options will be rendered as the attributes of the resulting tag. The values will + * be HTML-encoded using [[encode()]]. If a value is null, the corresponding attribute will not be rendered. + * + * @return string the generated drop-down list tag + */ + public static function dropDownList($name, $selection = null, $items = array(), $options = array()) + { + $options['name'] = $name; + $selectOptions = static::renderSelectOptions($selection, $items, $options); + return static::tag('select', "\n" . $selectOptions . "\n", $options); + } + + /** + * Generates a list box. + * @param string $name the input name + * @param string|array $selection the selected value(s) + * @param array $items the option data items. The array keys are option values, and the array values + * are the corresponding option labels. The array can also be nested (i.e. some array values are arrays too). + * For each sub-array, an option group will be generated whose label is the key associated with the sub-array. + * If you have a list of data models, you may convert them into the format described above using + * [[\yii\helpers\ArrayHelper::map()]]. + * + * Note, the values and labels will be automatically HTML-encoded by this method, and the blank spaces in + * the labels will also be HTML-encoded. + * @param array $options the tag options in terms of name-value pairs. The following options are supported: + * + * - prompt: string, a prompt text to be displayed as the first option; + * - options: array, the attributes for the select option tags. The array keys must be valid option values, + * and the array values are the extra attributes for the corresponding option tags. For example, + * + * ~~~ + * array( + * 'value1' => array('disabled' => true), + * 'value2' => array('label' => 'value 2'), + * ); + * ~~~ + * + * - groups: array, the attributes for the optgroup tags. The structure of this is similar to that of 'options', + * except that the array keys represent the optgroup labels specified in $items. + * - unselect: string, the value that will be submitted when no option is selected. + * When this attribute is set, a hidden field will be generated so that if no option is selected in multiple + * mode, we can still obtain the posted unselect value. + * + * The rest of the options will be rendered as the attributes of the resulting tag. The values will + * be HTML-encoded using [[encode()]]. If a value is null, the corresponding attribute will not be rendered. + * + * @return string the generated list box tag + */ + public static function listBox($name, $selection = null, $items = array(), $options = array()) + { + if (!isset($options['size'])) { + $options['size'] = 4; + } + if (isset($options['multiple']) && $options['multiple'] && substr($name, -2) !== '[]') { + $name .= '[]'; + } + $options['name'] = $name; + if (isset($options['unselect'])) { + // add a hidden field so that if the list box has no option being selected, it still submits a value + if (substr($name, -2) === '[]') { + $name = substr($name, 0, -2); + } + $hidden = static::hiddenInput($name, $options['unselect']); + unset($options['unselect']); + } else { + $hidden = ''; + } + $selectOptions = static::renderSelectOptions($selection, $items, $options); + return $hidden . static::tag('select', "\n" . $selectOptions . "\n", $options); + } + + /** + * Generates a list of checkboxes. + * A checkbox list allows multiple selection, like [[listBox()]]. + * As a result, the corresponding submitted value is an array. + * @param string $name the name attribute of each checkbox. + * @param string|array $selection the selected value(s). + * @param array $items the data item used to generate the checkboxes. + * The array keys are the labels, while the array values are the corresponding checkbox values. + * Note that the labels will NOT be HTML-encoded, while the values will. + * @param array $options options (name => config) for the checkbox list. The following options are supported: + * + * - unselect: string, the value that should be submitted when none of the checkboxes is selected. + * By setting this option, a hidden input will be generated. + * - separator: string, the HTML code that separates items. + * - item: callable, a callback that can be used to customize the generation of the HTML code + * corresponding to a single item in $items. The signature of this callback must be: + * + * ~~~ + * function ($index, $label, $name, $checked, $value) + * ~~~ + * + * where $index is the zero-based index of the checkbox in the whole list; $label + * is the label for the checkbox; and $name, $value and $checked represent the name, + * value and the checked status of the checkbox input. + * @return string the generated checkbox list + */ + public static function checkboxList($name, $selection = null, $items = array(), $options = array()) + { + if (substr($name, -2) !== '[]') { + $name .= '[]'; + } + + $formatter = isset($options['item']) ? $options['item'] : null; + $lines = array(); + $index = 0; + foreach ($items as $value => $label) { + $checked = $selection !== null && + (!is_array($selection) && !strcmp($value, $selection) + || is_array($selection) && in_array($value, $selection)); + if ($formatter !== null) { + $lines[] = call_user_func($formatter, $index, $label, $name, $checked, $value); + } else { + $lines[] = static::label(static::checkbox($name, $checked, $value) . ' ' . $label); + } + $index++; + } + + if (isset($options['unselect'])) { + // add a hidden field so that if the list box has no option being selected, it still submits a value + $name2 = substr($name, -2) === '[]' ? substr($name, 0, -2) : $name; + $hidden = static::hiddenInput($name2, $options['unselect']); + } else { + $hidden = ''; + } + $separator = isset($options['separator']) ? $options['separator'] : "\n"; + + return $hidden . implode($separator, $lines); + } + + /** + * Generates a list of radio buttons. + * A radio button list is like a checkbox list, except that it only allows single selection. + * @param string $name the name attribute of each radio button. + * @param string|array $selection the selected value(s). + * @param array $items the data item used to generate the radio buttons. + * The array keys are the labels, while the array values are the corresponding radio button values. + * Note that the labels will NOT be HTML-encoded, while the values will. + * @param array $options options (name => config) for the radio button list. The following options are supported: + * + * - unselect: string, the value that should be submitted when none of the radio buttons is selected. + * By setting this option, a hidden input will be generated. + * - separator: string, the HTML code that separates items. + * - item: callable, a callback that can be used to customize the generation of the HTML code + * corresponding to a single item in $items. The signature of this callback must be: + * + * ~~~ + * function ($index, $label, $name, $checked, $value) + * ~~~ + * + * where $index is the zero-based index of the radio button in the whole list; $label + * is the label for the radio button; and $name, $value and $checked represent the name, + * value and the checked status of the radio button input. + * @return string the generated radio button list + */ + public static function radioList($name, $selection = null, $items = array(), $options = array()) + { + $formatter = isset($options['item']) ? $options['item'] : null; + $lines = array(); + $index = 0; + foreach ($items as $value => $label) { + $checked = $selection !== null && + (!is_array($selection) && !strcmp($value, $selection) + || is_array($selection) && in_array($value, $selection)); + if ($formatter !== null) { + $lines[] = call_user_func($formatter, $index, $label, $name, $checked, $value); + } else { + $lines[] = static::label(static::radio($name, $checked, $value) . ' ' . $label); + } + $index++; + } + + $separator = isset($options['separator']) ? $options['separator'] : "\n"; + if (isset($options['unselect'])) { + // add a hidden field so that if the list box has no option being selected, it still submits a value + $hidden = static::hiddenInput($name, $options['unselect']); + } else { + $hidden = ''; + } + + return $hidden . implode($separator, $lines); + } + + /** + * Renders the option tags that can be used by [[dropDownList()]] and [[listBox()]]. + * @param string|array $selection the selected value(s). This can be either a string for single selection + * or an array for multiple selections. + * @param array $items the option data items. The array keys are option values, and the array values + * are the corresponding option labels. The array can also be nested (i.e. some array values are arrays too). + * For each sub-array, an option group will be generated whose label is the key associated with the sub-array. + * If you have a list of data models, you may convert them into the format described above using + * [[\yii\helpers\ArrayHelper::map()]]. + * + * Note, the values and labels will be automatically HTML-encoded by this method, and the blank spaces in + * the labels will also be HTML-encoded. + * @param array $tagOptions the $options parameter that is passed to the [[dropDownList()]] or [[listBox()]] call. + * This method will take out these elements, if any: "prompt", "options" and "groups". See more details + * in [[dropDownList()]] for the explanation of these elements. + * + * @return string the generated list options + */ + public static function renderSelectOptions($selection, $items, &$tagOptions = array()) + { + $lines = array(); + if (isset($tagOptions['prompt'])) { + $prompt = str_replace(' ', ' ', static::encode($tagOptions['prompt'])); + $lines[] = static::tag('option', $prompt, array('value' => '')); + } + + $options = isset($tagOptions['options']) ? $tagOptions['options'] : array(); + $groups = isset($tagOptions['groups']) ? $tagOptions['groups'] : array(); + unset($tagOptions['prompt'], $tagOptions['options'], $tagOptions['groups']); + + foreach ($items as $key => $value) { + if (is_array($value)) { + $groupAttrs = isset($groups[$key]) ? $groups[$key] : array(); + $groupAttrs['label'] = $key; + $attrs = array('options' => $options, 'groups' => $groups); + $content = static::renderSelectOptions($selection, $value, $attrs); + $lines[] = static::tag('optgroup', "\n" . $content . "\n", $groupAttrs); + } else { + $attrs = isset($options[$key]) ? $options[$key] : array(); + $attrs['value'] = $key; + $attrs['selected'] = $selection !== null && + (!is_array($selection) && !strcmp($key, $selection) + || is_array($selection) && in_array($key, $selection)); + $lines[] = static::tag('option', str_replace(' ', ' ', static::encode($value)), $attrs); + } + } + + return implode("\n", $lines); + } + + /** + * Renders the HTML tag attributes. + * Boolean attributes such as s 'checked', 'disabled', 'readonly', will be handled specially + * according to [[booleanAttributes]] and [[showBooleanAttributeValues]]. + * @param array $attributes attributes to be rendered. The attribute values will be HTML-encoded using [[encode()]]. + * Attributes whose value is null will be ignored and not put in the rendering result. + * @return string the rendering result. If the attributes are not empty, they will be rendered + * into a string with a leading white space (such that it can be directly appended to the tag name + * in a tag. If there is no attribute, an empty string will be returned. + */ + public static function renderTagAttributes($attributes) + { + if (count($attributes) > 1) { + $sorted = array(); + foreach (static::$attributeOrder as $name) { + if (isset($attributes[$name])) { + $sorted[$name] = $attributes[$name]; + } + } + $attributes = array_merge($sorted, $attributes); + } + + $html = ''; + foreach ($attributes as $name => $value) { + if (isset(static::$booleanAttributes[strtolower($name)])) { + if ($value || strcasecmp($name, $value) === 0) { + $html .= static::$showBooleanAttributeValues ? " $name=\"$name\"" : " $name"; + } + } elseif ($value !== null) { + $html .= " $name=\"" . static::encode($value) . '"'; + } + } + return $html; + } + + /** + * Normalizes the input parameter to be a valid URL. + * + * If the input parameter + * + * - is an empty string: the currently requested URL will be returned; + * - is a non-empty string: it will be processed by [[Yii::getAlias()]] and returned; + * - is an array: the first array element is considered a route, while the rest of the name-value + * pairs are treated as the parameters to be used for URL creation using [[\yii\web\Controller::createUrl()]]. + * For example: `array('post/index', 'page' => 2)`, `array('index')`. + * + * @param array|string $url the parameter to be used to generate a valid URL + * @return string the normalized URL + * @throws InvalidParamException if the parameter is invalid. + */ + public static function url($url) + { + if (is_array($url)) { + if (isset($url[0])) { + $route = $url[0]; + $params = array_splice($url, 1); + if (Yii::$app->controller !== null) { + return Yii::$app->controller->createUrl($route, $params); + } else { + return Yii::$app->getUrlManager()->createUrl($route, $params); + } + } else { + throw new InvalidParamException('The array specifying a URL must contain at least one element.'); + } + } elseif ($url === '') { + return Yii::$app->getRequest()->getUrl(); + } else { + return Yii::getAlias($url); + } + } +} diff --git a/framework/helpers/SecurityHelper.php b/framework/helpers/SecurityHelper.php new file mode 100644 index 0000000..5029dd6 --- /dev/null +++ b/framework/helpers/SecurityHelper.php @@ -0,0 +1,272 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\helpers; + +use Yii; +use yii\base\Exception; +use yii\base\InvalidConfigException; +use yii\base\InvalidParamException; + +/** + * SecurityHelper provides a set of methods to handle common security-related tasks. + * + * In particular, SecurityHelper supports the following features: + * + * - Encryption/decryption: [[encrypt()]] and [[decrypt()]] + * - Data tampering prevention: [[hashData()]] and [[validateData()]] + * - Password validation: [[generatePasswordHash()]] and [[validatePassword()]] + * + * Additionally, SecurityHelper provides [[getSecretKey()]] to support generating + * named secret keys. These secret keys, once generated, will be stored in a file + * and made available in future requests. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @author Tom Worster <fsb@thefsb.org> + * @since 2.0 + */ +class SecurityHelper +{ + /** + * Encrypts data. + * @param string $data data to be encrypted. + * @param string $key the encryption secret key + * @return string the encrypted data + * @throws Exception if PHP Mcrypt extension is not loaded or failed to be initialized + * @see decrypt() + */ + public static function encrypt($data, $key) + { + $module = static::openCryptModule(); + $key = StringHelper::substr($key, 0, mcrypt_enc_get_key_size($module)); + srand(); + $iv = mcrypt_create_iv(mcrypt_enc_get_iv_size($module), MCRYPT_RAND); + mcrypt_generic_init($module, $key, $iv); + $encrypted = $iv . mcrypt_generic($module, $data); + mcrypt_generic_deinit($module); + mcrypt_module_close($module); + return $encrypted; + } + + /** + * Decrypts data + * @param string $data data to be decrypted. + * @param string $key the decryption secret key + * @return string the decrypted data + * @throws Exception if PHP Mcrypt extension is not loaded or failed to be initialized + * @see encrypt() + */ + public static function decrypt($data, $key) + { + $module = static::openCryptModule(); + $key = StringHelper::substr($key, 0, mcrypt_enc_get_key_size($module)); + $ivSize = mcrypt_enc_get_iv_size($module); + $iv = StringHelper::substr($data, 0, $ivSize); + mcrypt_generic_init($module, $key, $iv); + $decrypted = mdecrypt_generic($module, StringHelper::substr($data, $ivSize, StringHelper::strlen($data))); + mcrypt_generic_deinit($module); + mcrypt_module_close($module); + return rtrim($decrypted, "\0"); + } + + /** + * Prefixes data with a keyed hash value so that it can later be detected if it is tampered. + * @param string $data the data to be protected + * @param string $key the secret key to be used for generating hash + * @param string $algorithm the hashing algorithm (e.g. "md5", "sha1", "sha256", etc.). Call PHP "hash_algos()" + * function to see the supported hashing algorithms on your system. + * @return string the data prefixed with the keyed hash + * @see validateData() + * @see getSecretKey() + */ + public static function hashData($data, $key, $algorithm = 'sha256') + { + return hash_hmac($algorithm, $data, $key) . $data; + } + + /** + * Validates if the given data is tampered. + * @param string $data the data to be validated. The data must be previously + * generated by [[hashData()]]. + * @param string $key the secret key that was previously used to generate the hash for the data in [[hashData()]]. + * @param string $algorithm the hashing algorithm (e.g. "md5", "sha1", "sha256", etc.). Call PHP "hash_algos()" + * function to see the supported hashing algorithms on your system. This must be the same + * as the value passed to [[hashData()]] when generating the hash for the data. + * @return string the real data with the hash stripped off. False if the data is tampered. + * @see hashData() + */ + public static function validateData($data, $key, $algorithm = 'sha256') + { + $hashSize = StringHelper::strlen(hash_hmac($algorithm, 'test', $key)); + $n = StringHelper::strlen($data); + if ($n >= $hashSize) { + $hash = StringHelper::substr($data, 0, $hashSize); + $data2 = StringHelper::substr($data, $hashSize, $n - $hashSize); + return $hash === hash_hmac($algorithm, $data2, $key) ? $data2 : false; + } else { + return false; + } + } + + /** + * Returns a secret key associated with the specified name. + * If the secret key does not exist, a random key will be generated + * and saved in the file "keys.php" under the application's runtime directory + * so that the same secret key can be returned in future requests. + * @param string $name the name that is associated with the secret key + * @param integer $length the length of the key that should be generated if not exists + * @return string the secret key associated with the specified name + */ + public static function getSecretKey($name, $length = 32) + { + static $keys; + $keyFile = Yii::$app->getRuntimePath() . '/keys.php'; + if ($keys === null) { + $keys = is_file($keyFile) ? require($keyFile) : array(); + } + if (!isset($keys[$name])) { + // generate a 32-char random key + $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + $keys[$name] = substr(str_shuffle(str_repeat($chars, 5)), 0, $length); + file_put_contents($keyFile, "<?php\nreturn " . var_export($keys, true) . ";\n"); + } + return $keys[$name]; + } + + /** + * Opens the mcrypt module. + * @return resource the mcrypt module handle. + * @throws InvalidConfigException if mcrypt extension is not installed + * @throws Exception if mcrypt initialization fails + */ + protected static function openCryptModule() + { + if (!extension_loaded('mcrypt')) { + throw new InvalidConfigException('The mcrypt PHP extension is not installed.'); + } + $module = @mcrypt_module_open('rijndael-256', '', MCRYPT_MODE_CBC, ''); + if ($module === false) { + throw new Exception('Failed to initialize the mcrypt module.'); + } + return $module; + } + + /** + * Generates a secure hash from a password and a random salt. + * + * The generated hash can be stored in database (e.g. `CHAR(64) CHARACTER SET latin1` on MySQL). + * Later when a password needs to be validated, the hash can be fetched and passed + * to [[validatePassword()]]. For example, + * + * ~~~ + * // generates the hash (usually done during user registration or when the password is changed) + * $hash = SecurityHelper::hashPassword($password); + * // ...save $hash in database... + * + * // during login, validate if the password entered is correct using $hash fetched from database + * if (PasswordHelper::verifyPassword($password, $hash) { + * // password is good + * } else { + * // password is bad + * } + * ~~~ + * + * @param string $password The password to be hashed. + * @param integer $cost Cost parameter used by the Blowfish hash algorithm. + * The higher the value of cost, + * the longer it takes to generate the hash and to verify a password against it. Higher cost + * therefore slows down a brute-force attack. For best protection against brute for attacks, + * set it to the highest value that is tolerable on production servers. The time taken to + * compute the hash doubles for every increment by one of $cost. So, for example, if the + * hash takes 1 second to compute when $cost is 14 then then the compute time varies as + * 2^($cost - 14) seconds. + * @throws Exception on bad password parameter or cost parameter + * @return string The password hash string, ASCII and not longer than 64 characters. + * @see validatePassword() + */ + public static function generatePasswordHash($password, $cost = 13) + { + $salt = static::generateSalt($cost); + $hash = crypt($password, $salt); + + if (!is_string($hash) || strlen($hash) < 32) { + throw new Exception('Unknown error occurred while generating hash.'); + } + + return $hash; + } + + /** + * Verifies a password against a hash. + * @param string $password The password to verify. + * @param string $hash The hash to verify the password against. + * @return boolean whether the password is correct. + * @throws InvalidParamException on bad password or hash parameters or if crypt() with Blowfish hash is not available. + * @see generatePasswordHash() + */ + public static function validatePassword($password, $hash) + { + if (!is_string($password) || $password === '') { + throw new InvalidParamException('Password must be a string and cannot be empty.'); + } + + if (!preg_match('/^\$2[axy]\$(\d\d)\$[\./0-9A-Za-z]{22}/', $hash, $matches) || $matches[1] < 4 || $matches[1] > 30) { + throw new InvalidParamException('Hash is invalid.'); + } + + $test = crypt($password, $hash); + $n = strlen($test); + if (strlen($test) < 32 || $n !== strlen($hash)) { + return false; + } + + // Use a for-loop to compare two strings to prevent timing attacks. See: + // http://codereview.stackexchange.com/questions/13512 + $check = 0; + for ($i = 0; $i < $n; ++$i) { + $check |= (ord($test[$i]) ^ ord($hash[$i])); + } + + return $check === 0; + } + + /** + * Generates a salt that can be used to generate a password hash. + * + * The PHP [crypt()](http://php.net/manual/en/function.crypt.php) built-in function + * requires, for the Blowfish hash algorithm, a salt string in a specific format: + * "$2a$", "$2x$" or "$2y$", a two digit cost parameter, "$", and 22 characters + * from the alphabet "./0-9A-Za-z". + * + * @param integer $cost the cost parameter + * @return string the random salt value. + * @throws InvalidParamException if the cost parameter is not between 4 and 30 + */ + protected static function generateSalt($cost = 13) + { + $cost = (int)$cost; + if ($cost < 4 || $cost > 30) { + throw new InvalidParamException('Cost must be between 4 and 31.'); + } + + // Get 20 * 8bits of pseudo-random entropy from mt_rand(). + $rand = ''; + for ($i = 0; $i < 20; ++$i) { + $rand .= chr(mt_rand(0, 255)); + } + + // Add the microtime for a little more entropy. + $rand .= microtime(); + // Mix the bits cryptographically into a 20-byte binary string. + $rand = sha1($rand, true); + // Form the prefix that specifies Blowfish algorithm and cost parameter. + $salt = sprintf("$2y$%02d$", $cost); + // Append the random salt data in the required base64 format. + $salt .= str_replace('+', '.', substr(base64_encode($rand), 0, 22)); + return $salt; + } +} \ No newline at end of file diff --git a/framework/util/StringHelper.php b/framework/helpers/StringHelper.php similarity index 83% rename from framework/util/StringHelper.php rename to framework/helpers/StringHelper.php index 776657e..ace34db 100644 --- a/framework/util/StringHelper.php +++ b/framework/helpers/StringHelper.php @@ -1,13 +1,11 @@ <?php /** - * StringHelper class file. - * * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ -namespace yii\util; +namespace yii\helpers; /** * StringHelper @@ -19,6 +17,33 @@ namespace yii\util; class StringHelper { /** + * Returns the number of bytes in the given string. + * This method ensures the string is treated as a byte array. + * It will use `mb_strlen()` if it is available. + * @param string $string the string being measured for length + * @return integer the number of bytes in the given string. + */ + public static function strlen($string) + { + return function_exists('mb_strlen') ? mb_strlen($string, '8bit') : strlen($string); + } + + /** + * Returns the portion of string specified by the start and length parameters. + * This method ensures the string is treated as a byte array. + * It will use `mb_substr()` if it is available. + * @param string $string the input string. Must be one character or longer. + * @param integer $start the starting position + * @param integer $length the desired portion length + * @return string the extracted part of string, or FALSE on failure or an empty string. + * @see http://www.php.net/manual/en/function.substr.php + */ + public static function substr($string, $start, $length) + { + return function_exists('mb_substr') ? mb_substr($string, $start, $length, '8bit') : substr($string, $start, $length); + } + + /** * Converts a word to its plural form. * Note that this is for English only! * For example, 'apple' will become 'apples', and 'child' will become 'children'. @@ -27,7 +52,7 @@ class StringHelper */ public static function pluralize($name) { - $rules = array( + static $rules = array( '/(m)ove$/i' => '\1oves', '/(f)oot$/i' => '\1eet', '/(c)hild$/i' => '\1hildren', diff --git a/framework/util/VarDumper.php b/framework/helpers/VarDumper.php similarity index 56% rename from framework/util/VarDumper.php rename to framework/helpers/VarDumper.php index 7497a03..64c3639 100644 --- a/framework/util/VarDumper.php +++ b/framework/helpers/VarDumper.php @@ -1,14 +1,12 @@ <?php /** - * VarDumper class file. - * * @author Qiang Xue <qiang.xue@gmail.com> * @link http://www.yiiframework.com/ * @copyright Copyright © 2008-2011 Yii Software LLC * @license http://www.yiiframework.com/license/ */ -namespace yii\util; +namespace yii\helpers; /** * VarDumper is intended to replace the buggy PHP function var_dump and print_r. @@ -17,14 +15,15 @@ namespace yii\util; * recursive display of some peculiar variables. * * VarDumper can be used as follows, - * <pre> + * + * ~~~ * VarDumper::dump($var); - * </pre> + * ~~~ * * @author Qiang Xue <qiang.xue@gmail.com> * @since 2.0 */ -class VarDumper +class CVarDumper { private static $_objects; private static $_output; @@ -38,9 +37,9 @@ class VarDumper * @param integer $depth maximum depth that the dumper should go into the variable. Defaults to 10. * @param boolean $highlight whether the result should be syntax-highlighted */ - public static function dump($var,$depth=10,$highlight=false) + public static function dump($var, $depth = 10, $highlight = false) { - echo self::dumpAsString($var,$depth,$highlight); + echo self::dumpAsString($var, $depth, $highlight); } /** @@ -52,16 +51,15 @@ class VarDumper * @param boolean $highlight whether the result should be syntax-highlighted * @return string the string representation of the variable */ - public static function dumpAsString($var,$depth=10,$highlight=false) + public static function dumpAsString($var, $depth = 10, $highlight = false) { - self::$_output=''; - self::$_objects=array(); - self::$_depth=$depth; - self::dumpInternal($var,0); - if($highlight) - { - $result=highlight_string("<?php\n".self::$_output,true); - self::$_output=preg_replace('/<\\?php<br \\/>/','',$result,1); + self::$_output = ''; + self::$_objects = array(); + self::$_depth = $depth; + self::dumpInternal($var, 0); + if ($highlight) { + $result = highlight_string("<?php\n" . self::$_output, true); + self::$_output = preg_replace('/<\\?php<br \\/>/', '', $result, 1); } return self::$_output; } @@ -70,73 +68,65 @@ class VarDumper * @param mixed $var variable to be dumped * @param integer $level depth level */ - private static function dumpInternal($var,$level) + private static function dumpInternal($var, $level) { - switch(gettype($var)) - { + switch (gettype($var)) { case 'boolean': - self::$_output.=$var?'true':'false'; + self::$_output .= $var ? 'true' : 'false'; break; case 'integer': - self::$_output.="$var"; + self::$_output .= "$var"; break; case 'double': - self::$_output.="$var"; + self::$_output .= "$var"; break; case 'string': - self::$_output.="'".addslashes($var)."'"; + self::$_output .= "'" . addslashes($var) . "'"; break; case 'resource': - self::$_output.='{resource}'; + self::$_output .= '{resource}'; break; case 'NULL': - self::$_output.="null"; + self::$_output .= "null"; break; case 'unknown type': - self::$_output.='{unknown}'; + self::$_output .= '{unknown}'; break; case 'array': - if(self::$_depth<=$level) - self::$_output.='array(...)'; - else if(empty($var)) - self::$_output.='array()'; - else - { - $keys=array_keys($var); - $spaces=str_repeat(' ',$level*4); - self::$_output.="array\n".$spaces.'('; - foreach($keys as $key) - { - if(gettype($key)=='integer') - $key2=$key; - else - $key2="'".str_replace("'","\\'",$key)."'"; - - self::$_output.="\n".$spaces." $key2 => "; - self::$_output.=self::dumpInternal($var[$key],$level+1); + if (self::$_depth <= $level) { + self::$_output .= 'array(...)'; + } elseif (empty($var)) { + self::$_output .= 'array()'; + } else { + $keys = array_keys($var); + $spaces = str_repeat(' ', $level * 4); + self::$_output .= "array\n" . $spaces . '('; + foreach ($keys as $key) { + self::$_output .= "\n" . $spaces . ' '; + self::dumpInternal($key, 0); + self::$_output .= ' => '; + self::dumpInternal($var[$key], $level + 1); } - self::$_output.="\n".$spaces.')'; + self::$_output .= "\n" . $spaces . ')'; } break; case 'object': - if(($id=array_search($var,self::$_objects,true))!==false) - self::$_output.=get_class($var).'#'.($id+1).'(...)'; - else if(self::$_depth<=$level) - self::$_output.=get_class($var).'(...)'; - else - { - $id=array_push(self::$_objects,$var); - $className=get_class($var); - $members=(array)$var; - $spaces=str_repeat(' ',$level*4); - self::$_output.="$className#$id\n".$spaces.'('; - foreach($members as $key=>$value) - { - $keyDisplay=strtr(trim($key),array("\0"=>':')); - self::$_output.="\n".$spaces." [$keyDisplay] => "; - self::$_output.=self::dumpInternal($value,$level+1); + if (($id = array_search($var, self::$_objects, true)) !== false) { + self::$_output .= get_class($var) . '#' . ($id + 1) . '(...)'; + } elseif (self::$_depth <= $level) { + self::$_output .= get_class($var) . '(...)'; + } else { + $id = self::$_objects[] = $var; + $className = get_class($var); + $members = (array)$var; + $spaces = str_repeat(' ', $level * 4); + self::$_output .= "$className#$id\n" . $spaces . '('; + foreach ($members as $key => $value) { + $keyDisplay = strtr(trim($key), array("\0" => ':')); + self::$_output .= "\n" . $spaces . " [$keyDisplay] => "; + self::dumpInternal($value, $level + 1); } - self::$_output.="\n".$spaces.')'; + self::$_output .= "\n" . $spaces . ')'; } break; } diff --git a/framework/util/mimeTypes.php b/framework/helpers/mimeTypes.php similarity index 99% rename from framework/util/mimeTypes.php rename to framework/helpers/mimeTypes.php index 87295f8..ffdba4b 100644 --- a/framework/util/mimeTypes.php +++ b/framework/helpers/mimeTypes.php @@ -6,7 +6,7 @@ * according to file extension names. * * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ * @since 2.0 */ diff --git a/framework/i18n/I18N.php b/framework/i18n/I18N.php new file mode 100644 index 0000000..0409da3 --- /dev/null +++ b/framework/i18n/I18N.php @@ -0,0 +1,119 @@ +<?php + +namespace yii\i18n; + +use Yii; +use yii\base\Component; +use yii\base\InvalidConfigException; + +class I18N extends Component +{ + /** + * @var array list of [[MessageSource]] configurations or objects. The array keys are message + * categories, and the array values are the corresponding [[MessageSource]] objects or the configurations + * for creating the [[MessageSource]] objects. The message categories can contain the wildcard '*' at the end + * to match multiple categories with the same prefix. For example, 'app\*' matches both 'app\cat1' and 'app\cat2'. + */ + public $translations; + + public function init() + { + if (!isset($this->translations['yii'])) { + $this->translations['yii'] = array( + 'class' => 'yii\i18n\PhpMessageSource', + 'sourceLanguage' => 'en_US', + 'basePath' => '@yii/messages', + ); + } + if (!isset($this->translations['app'])) { + $this->translations['app'] = array( + 'class' => 'yii\i18n\PhpMessageSource', + 'sourceLanguage' => 'en_US', + 'basePath' => '@app/messages', + ); + } + } + + public function translate($message, $params = array(), $language = null) + { + if ($language === null) { + $language = Yii::$app->language; + } + + // allow chars for category: word chars, ".", "-", "/","\" + if (strpos($message, '|') !== false && preg_match('/^([\w\-\\/\.\\\\]+)\|(.*)/', $message, $matches)) { + $category = $matches[1]; + $message = $matches[2]; + } else { + $category = 'app'; + } + + $message = $this->getMessageSource($category)->translate($category, $message, $language); + + if (!is_array($params)) { + $params = array($params); + } + + if (isset($params[0])) { + $message = $this->getPluralForm($message, $params[0], $language); + if (!isset($params['{n}'])) { + $params['{n}'] = $params[0]; + } + unset($params[0]); + } + + return $params === array() ? $message : strtr($message, $params); + } + + public function getMessageSource($category) + { + if (isset($this->translations[$category])) { + $source = $this->translations[$category]; + } else { + // try wildcard matching + foreach ($this->translations as $pattern => $config) { + if (substr($pattern, -1) === '*' && strpos($category, rtrim($pattern, '*')) === 0) { + $source = $config; + break; + } + } + } + if (isset($source)) { + return $source instanceof MessageSource ? $source : Yii::createObject($source); + } else { + throw new InvalidConfigException("Unable to locate message source for category '$category'."); + } + } + + public function getLocale($language) + { + + } + + protected function getPluralForm($message, $number, $language) + { + if (strpos($message, '|') === false) { + return $message; + } + $chunks = explode('|', $message); + $rules = $this->getLocale($language)->getPluralRules(); + foreach ($rules as $i => $rule) { + if (isset($chunks[$i]) && $this->evaluate($rule, $number)) { + return $chunks[$i]; + } + } + $n = count($rules); + return isset($chunks[$n]) ? $chunks[$n] : $chunks[0]; + } + + /** + * Evaluates a PHP expression with the given number value. + * @param string $expression the PHP expression + * @param mixed $n the number value + * @return boolean the expression result + */ + protected function evaluate($expression, $n) + { + return @eval("return $expression;"); + } +} diff --git a/framework/i18n/MessageSource.php b/framework/i18n/MessageSource.php new file mode 100644 index 0000000..cf23338 --- /dev/null +++ b/framework/i18n/MessageSource.php @@ -0,0 +1,121 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\i18n; + +use Yii; +use yii\base\Component; + +/** + * MessageSource is the base class for message translation repository classes. + * + * A message source stores message translations in some persistent storage. + * + * Child classes should override [[loadMessages()]] to provide translated messages. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class MessageSource extends Component +{ + /** + * @event MissingTranslationEvent an event that is triggered when a message translation is not found. + */ + const EVENT_MISSING_TRANSLATION = 'missingTranslation'; + + /** + * @var boolean whether to force message translation when the source and target languages are the same. + * Defaults to false, meaning translation is only performed when source and target languages are different. + */ + public $forceTranslation = false; + /** + * @var string the language that the original messages are in. If not set, it will use the value of + * [[\yii\base\Application::sourceLanguage]]. + */ + public $sourceLanguage; + + private $_messages = array(); + + /** + * Initializes this component. + */ + public function init() + { + parent::init(); + if ($this->sourceLanguage === null) { + $this->sourceLanguage = Yii::$app->sourceLanguage; + } + } + + /** + * Loads the message translation for the specified language and category. + * Child classes should override this method to return the message translations of + * the specified language and category. + * @param string $category the message category + * @param string $language the target language + * @return array the loaded messages. The keys are original messages, and the values + * are translated messages. + */ + protected function loadMessages($category, $language) + { + return array(); + } + + /** + * Translates a message to the specified language. + * + * Note that unless [[forceTranslation]] is true, if the target language + * is the same as the [[sourceLanguage|source language]], the message + * will NOT be translated. + * + * If a translation is not found, a [[missingTranslation]] event will be triggered. + * + * @param string $category the message category + * @param string $message the message to be translated + * @param string $language the target language + * @return string the translated message (or the original message if translation is not needed) + */ + public function translate($category, $message, $language) + { + if ($this->forceTranslation || $language !== $this->sourceLanguage) { + return $this->translateMessage($category, $message, $language); + } else { + return $message; + } + } + + /** + * Translates the specified message. + * If the message is not found, a [[missingTranslation]] event will be triggered + * and the original message will be returned. + * @param string $category the category that the message belongs to + * @param string $message the message to be translated + * @param string $language the target language + * @return string the translated message + */ + protected function translateMessage($category, $message, $language) + { + $key = $language . '/' . $category; + if (!isset($this->_messages[$key])) { + $this->_messages[$key] = $this->loadMessages($category, $language); + } + if (isset($this->_messages[$key][$message]) && $this->_messages[$key][$message] !== '') { + return $this->_messages[$key][$message]; + } elseif ($this->hasEventHandlers('missingTranslation')) { + $event = new MissingTranslationEvent(array( + 'category' => $category, + 'message' => $message, + 'language' => $language, + )); + $this->trigger(self::EVENT_MISSING_TRANSLATION, $event); + return $this->_messages[$key] = $event->message; + } else { + return $message; + } + } +} + diff --git a/framework/i18n/MissingTranslationEvent.php b/framework/i18n/MissingTranslationEvent.php new file mode 100644 index 0000000..9ac337a --- /dev/null +++ b/framework/i18n/MissingTranslationEvent.php @@ -0,0 +1,33 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\i18n; + +use yii\base\Event; + +/** + * MissingTranslationEvent represents the parameter for the [[MessageSource::missingTranslation]] event. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class MissingTranslationEvent extends Event +{ + /** + * @var string the message to be translated. An event handler may overwrite this property + * with a translated version if possible. + */ + public $message; + /** + * @var string the category that the message belongs to + */ + public $category; + /** + * @var string the language ID (e.g. en_US) that the message is to be translated to + */ + public $language; +} diff --git a/framework/i18n/PhpMessageSource.php b/framework/i18n/PhpMessageSource.php new file mode 100644 index 0000000..6b12353 --- /dev/null +++ b/framework/i18n/PhpMessageSource.php @@ -0,0 +1,79 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\i18n; + +use Yii; + +/** + * PhpMessageSource represents a message source that stores translated messages in PHP scripts. + * + * PhpMessageSource uses PHP arrays to keep message translations. + * + * - Each PHP script contains one array which stores the message translations in one particular + * language and for a single message category; + * - Each PHP script is saved as a file named as `[[basePath]]/LanguageID/CategoryName.php`; + * - Within each PHP script, the message translations are returned as an array like the following: + * + * ~~~ + * return array( + * 'original message 1' => 'translated message 1', + * 'original message 2' => 'translated message 2', + * ); + * ~~~ + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class PhpMessageSource extends MessageSource +{ + /** + * @var string the base path for all translated messages. Defaults to null, meaning + * the "messages" subdirectory of the application directory (e.g. "protected/messages"). + */ + public $basePath = '@app/messages'; + /** + * @var array mapping between message categories and the corresponding message file paths. + * The file paths are relative to [[basePath]]. For example, + * + * ~~~ + * array( + * 'core' => 'core.php', + * 'ext' => 'extensions.php', + * ) + * ~~~ + */ + public $fileMap; + + /** + * Loads the message translation for the specified language and category. + * @param string $category the message category + * @param string $language the target language + * @return array the loaded messages + */ + protected function loadMessages($category, $language) + { + $messageFile = Yii::getAlias($this->basePath) . "/$language/"; + if (isset($this->fileMap[$category])) { + $messageFile .= $this->fileMap[$category]; + } elseif (($pos = strrpos($category, '\\')) !== false) { + $messageFile .= (substr($category, $pos) . '.php'); + } else { + $messageFile .= "$category.php"; + } + if (is_file($messageFile)) { + $messages = include($messageFile); + if (!is_array($messages)) { + $messages = array(); + } + return $messages; + } else { + Yii::error("The message file for category '$category' does not exist: $messageFile", __CLASS__); + return array(); + } + } +} \ No newline at end of file diff --git a/framework/logging/DbTarget.php b/framework/logging/DbTarget.php index 129e4d4..e4e30ce 100644 --- a/framework/logging/DbTarget.php +++ b/framework/logging/DbTarget.php @@ -1,24 +1,21 @@ <?php /** - * DbTarget class file. - * * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ namespace yii\logging; +use Yii; use yii\db\Connection; use yii\base\InvalidConfigException; /** * DbTarget stores log messages in a database table. * - * By default, DbTarget will use the database specified by [[connectionID]] and save - * messages into a table named by [[tableName]]. Please refer to [[tableName]] for the required - * table structure. Note that this table must be created beforehand. Otherwise an exception - * will be thrown when DbTarget is saving messages into DB. + * By default, DbTarget stores the log messages in a DB table named 'tbl_log'. This table + * must be pre-created. The table name can be changed by setting [[logTable]]. * * @author Qiang Xue <qiang.xue@gmail.com> * @since 2.0 @@ -26,20 +23,18 @@ use yii\base\InvalidConfigException; class DbTarget extends Target { /** - * @var string the ID of [[Connection]] application component. - * Defaults to 'db'. Please make sure that your database contains a table - * whose name is as specified in [[tableName]] and has the required table structure. - * @see tableName + * @var Connection|string the DB connection object or the application component ID of the DB connection. + * After the DbTarget object is created, if you want to change this property, you should only assign it + * with a DB connection object. */ - public $connectionID = 'db'; + public $db = 'db'; /** - * @var string the name of the DB table that stores log messages. Defaults to 'tbl_log'. - * - * The DB table should have the following structure: + * @var string name of the DB table to store cache content. + * The table should be pre-created as follows: * * ~~~ * CREATE TABLE tbl_log ( - * id INTEGER NOT NULL AUTO_INCREMENT PRIMARY KEY, + * id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, * level INTEGER, * category VARCHAR(255), * log_time INTEGER, @@ -50,42 +45,29 @@ class DbTarget extends Target * ~~~ * * Note that the 'id' column must be created as an auto-incremental column. - * The above SQL shows the syntax of MySQL. If you are using other DBMS, you need + * The above SQL uses the MySQL syntax. If you are using other DBMS, you need * to adjust it accordingly. For example, in PostgreSQL, it should be `id SERIAL PRIMARY KEY`. * * The indexes declared above are not required. They are mainly used to improve the performance * of some queries about message levels and categories. Depending on your actual needs, you may - * want to create additional indexes (e.g. index on log_time). + * want to create additional indexes (e.g. index on `log_time`). */ - public $tableName = 'tbl_log'; - - private $_db; + public $logTable = 'tbl_log'; /** - * Returns the DB connection used for saving log messages. - * @return Connection the DB connection instance - * @throws InvalidConfigException if [[connectionID]] does not point to a valid application component. + * Initializes the DbTarget component. + * This method will initialize the [[db]] property to make sure it refers to a valid DB connection. + * @throws InvalidConfigException if [[db]] is invalid. */ - public function getDb() + public function init() { - if ($this->_db === null) { - $db = \Yii::$application->getComponent($this->connectionID); - if ($db instanceof Connection) { - $this->_db = $db; - } else { - throw new InvalidConfigException("DbTarget::connectionID must refer to the ID of a DB application component."); - } + parent::init(); + if (is_string($this->db)) { + $this->db = Yii::$app->getComponent($this->db); + } + if (!$this->db instanceof Connection) { + throw new InvalidConfigException("DbTarget::db must be either a DB connection instance or the application component ID of a DB connection."); } - return $this->_db; - } - - /** - * Sets the DB connection used by the cache component. - * @param Connection $value the DB connection instance - */ - public function setDb($value) - { - $this->_db = $value; } /** @@ -95,10 +77,9 @@ class DbTarget extends Target */ public function export($messages) { - $db = $this->getDb(); - $tableName = $db->quoteTableName($this->tableName); + $tableName = $this->db->quoteTableName($this->logTable); $sql = "INSERT INTO $tableName (level, category, log_time, message) VALUES (:level, :category, :log_time, :message)"; - $command = $db->createCommand($sql); + $command = $this->db->createCommand($sql); foreach ($messages as $message) { $command->bindValues(array( ':level' => $message[1], diff --git a/framework/logging/EmailTarget.php b/framework/logging/EmailTarget.php index e02e4da..4c84739 100644 --- a/framework/logging/EmailTarget.php +++ b/framework/logging/EmailTarget.php @@ -1,9 +1,7 @@ <?php /** - * EmailTarget class file. - * * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ @@ -50,7 +48,7 @@ class EmailTarget extends Target $body .= $this->formatMessage($message); } $body = wordwrap($body, 70); - $subject = $this->subject === null ? \Yii::t('yii', 'Application Log') : $this->subject; + $subject = $this->subject === null ? \Yii::t('yii|Application Log') : $this->subject; foreach ($this->emails as $email) { $this->sendEmail($subject, $body, $email, $this->sentFrom, $this->headers); } diff --git a/framework/logging/FileTarget.php b/framework/logging/FileTarget.php index 0eb897e..c3f4031 100644 --- a/framework/logging/FileTarget.php +++ b/framework/logging/FileTarget.php @@ -1,9 +1,7 @@ <?php /** - * FileTarget class file. - * * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ @@ -48,7 +46,7 @@ class FileTarget extends Target { parent::init(); if ($this->logFile === null) { - $this->logFile = \Yii::$application->getRuntimePath() . DIRECTORY_SEPARATOR . 'application.log'; + $this->logFile = \Yii::$app->getRuntimePath() . DIRECTORY_SEPARATOR . 'application.log'; } else { $this->logFile = \Yii::getAlias($this->logFile); } diff --git a/framework/logging/Logger.php b/framework/logging/Logger.php index a8ffb5e..607c388 100644 --- a/framework/logging/Logger.php +++ b/framework/logging/Logger.php @@ -1,9 +1,7 @@ <?php /** - * Logger class file - * * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ diff --git a/framework/logging/ProfileTarget.php b/framework/logging/ProfileTarget.php index 716e6b7..2b6ffe6 100644 --- a/framework/logging/ProfileTarget.php +++ b/framework/logging/ProfileTarget.php @@ -1,7 +1,5 @@ <?php /** - * CProfileLogRoute class file. - * * @link http://www.yiiframework.com/ * @copyright Copyright © 2008-2011 Yii Software LLC * @license http://www.yiiframework.com/license/ @@ -61,7 +59,7 @@ class CProfileLogRoute extends CWebLogRoute if ($value === 'summary' || $value === 'callstack') $this->_report = $value; else - throw new CException(Yii::t('yii', 'CProfileLogRoute.report "{report}" is invalid. Valid values include "summary" and "callstack".', + throw new CException(Yii::t('yii|CProfileLogRoute.report "{report}" is invalid. Valid values include "summary" and "callstack".', array('{report}' => $value))); } @@ -71,7 +69,7 @@ class CProfileLogRoute extends CWebLogRoute */ public function processLogs($logs) { - $app = \Yii::$application; + $app = \Yii::$app; if (!($app instanceof CWebApplication) || $app->getRequest()->getIsAjaxRequest()) return; @@ -108,7 +106,7 @@ class CProfileLogRoute extends CWebLogRoute $results[$last[4]] = array($token, $delta, count($stack)); } else { - throw new CException(Yii::t('yii', 'CProfileLogRoute found a mismatching code block "{token}". Make sure the calls to Yii::beginProfile() and Yii::endProfile() be properly nested.', + throw new CException(Yii::t('yii|CProfileLogRoute found a mismatching code block "{token}". Make sure the calls to Yii::beginProfile() and Yii::endProfile() be properly nested.', array('{token}' => $token))); } } @@ -151,7 +149,7 @@ class CProfileLogRoute extends CWebLogRoute else $results[$token] = array($token, 1, $delta, $delta, $delta); } else - throw new CException(Yii::t('yii', 'CProfileLogRoute found a mismatching code block "{token}". Make sure the calls to Yii::beginProfile() and Yii::endProfile() be properly nested.', + throw new CException(Yii::t('yii|CProfileLogRoute found a mismatching code block "{token}". Make sure the calls to Yii::beginProfile() and Yii::endProfile() be properly nested.', array('{token}' => $token))); } } diff --git a/framework/logging/Router.php b/framework/logging/Router.php index 2e6a8dd..2f399fe 100644 --- a/framework/logging/Router.php +++ b/framework/logging/Router.php @@ -1,9 +1,7 @@ <?php /** - * Router class file. - * * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ @@ -52,7 +50,7 @@ use yii\base\Application; * as follows: * * ~~~ - * Yii::$application->log->targets['file']->enabled = false; + * Yii::$app->log->targets['file']->enabled = false; * ~~~ * * @author Qiang Xue <qiang.xue@gmail.com> diff --git a/framework/logging/Target.php b/framework/logging/Target.php index c9e175a..b88e78d 100644 --- a/framework/logging/Target.php +++ b/framework/logging/Target.php @@ -1,9 +1,7 @@ <?php /** - * Target class file. - * * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ @@ -110,7 +108,7 @@ abstract class Target extends \yii\base\Component protected function getContextMessage() { $context = array(); - if ($this->logUser && ($user = \Yii::$application->getComponent('user', false)) !== null) { + if ($this->logUser && ($user = \Yii::$app->getComponent('user', false)) !== null) { $context[] = 'User: ' . $user->getName() . ' (ID: ' . $user->getId() . ')'; } @@ -194,8 +192,7 @@ abstract class Target extends \yii\base\Component $matched = empty($this->categories); foreach ($this->categories as $category) { - $prefix = rtrim($category, '*'); - if (strpos($message[2], $prefix) === 0 && ($message[2] === $category || $prefix !== $category)) { + if ($message[2] === $category || substr($category, -1) === '*' && strpos($message[2], rtrim($category, '*')) === 0) { $matched = true; break; } diff --git a/framework/logging/WebTarget.php b/framework/logging/WebTarget.php index 6ce8ea0..b71e1a2 100644 --- a/framework/logging/WebTarget.php +++ b/framework/logging/WebTarget.php @@ -1,7 +1,5 @@ <?php /** - * CWebLogRoute class file. - * * @link http://www.yiiframework.com/ * @copyright Copyright © 2008-2011 Yii Software LLC * @license http://www.yiiframework.com/license/ @@ -46,7 +44,7 @@ class CWebLogRoute extends CLogRoute */ protected function render($view, $data) { - $app = \Yii::$application; + $app = \Yii::$app; $isAjax = $app->getRequest()->getIsAjaxRequest(); if ($this->showInFireBug) diff --git a/framework/test/TestCase.php b/framework/test/TestCase.php index 959bb96..f190e5a 100644 --- a/framework/test/TestCase.php +++ b/framework/test/TestCase.php @@ -3,16 +3,16 @@ * TestCase class. * * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ namespace yii\test; require_once('PHPUnit/Runner/Version.php'); -spl_autoload_unregister(array('YiiBase','autoload')); +spl_autoload_unregister(array('Yii','autoload')); require_once('PHPUnit/Autoload.php'); -spl_autoload_register(array('YiiBase','autoload')); // put yii's autoloader at the end +spl_autoload_register(array('Yii','autoload')); // put yii's autoloader at the end /** * TestCase is the base class for all test case classes. diff --git a/framework/test/WebTestCase.php b/framework/test/WebTestCase.php new file mode 100644 index 0000000..39162c9 --- /dev/null +++ b/framework/test/WebTestCase.php @@ -0,0 +1,25 @@ +<?php +/** + * WebTestCase class. + * + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\test; + +require_once('PHPUnit/Runner/Version.php'); +spl_autoload_unregister(array('Yii','autoload')); +require_once('PHPUnit/Autoload.php'); +spl_autoload_register(array('Yii','autoload')); // put yii's autoloader at the end + +/** + * WebTestCase is the base class for all test case classes. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +abstract class WebTestCase extends \PHPUnit_Extensions_SeleniumTestCase +{ +} diff --git a/framework/validators/BooleanValidator.php b/framework/validators/BooleanValidator.php index 90e7939..427fa44 100644 --- a/framework/validators/BooleanValidator.php +++ b/framework/validators/BooleanValidator.php @@ -1,9 +1,7 @@ <?php /** - * BooleanValidator class file. - * * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ @@ -54,7 +52,7 @@ class BooleanValidator extends Validator } if (!$this->strict && $value != $this->trueValue && $value != $this->falseValue || $this->strict && $value !== $this->trueValue && $value !== $this->falseValue) { - $message = ($this->message !== null) ? $this->message : \Yii::t('yii', '{attribute} must be either {true} or {false}.'); + $message = ($this->message !== null) ? $this->message : \Yii::t('yii|{attribute} must be either {true} or {false}.'); $this->addError($object, $attribute, $message, array( '{true}' => $this->trueValue, '{false}' => $this->falseValue, @@ -70,7 +68,7 @@ class BooleanValidator extends Validator */ public function clientValidateAttribute($object, $attribute) { - $message = ($this->message !== null) ? $this->message : \Yii::t('yii', '{attribute} must be either {true} or {false}.'); + $message = ($this->message !== null) ? $this->message : \Yii::t('yii|{attribute} must be either {true} or {false}.'); $message = strtr($message, array( '{attribute}' => $object->getAttributeLabel($attribute), '{value}' => $object->$attribute, diff --git a/framework/validators/CaptchaValidator.php b/framework/validators/CaptchaValidator.php index 3da8ed6..3f31f77 100644 --- a/framework/validators/CaptchaValidator.php +++ b/framework/validators/CaptchaValidator.php @@ -1,9 +1,7 @@ <?php /** - * CaptchaValidator class file. - * * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ @@ -49,7 +47,7 @@ class CaptchaValidator extends Validator } $captcha = $this->getCaptchaAction(); if (!$captcha->validate($value, $this->caseSensitive)) { - $message = $this->message !== null ? $this->message : \Yii::t('yii', 'The verification code is incorrect.'); + $message = $this->message !== null ? $this->message : \Yii::t('yii|The verification code is incorrect.'); $this->addError($object, $attribute, $message); } } @@ -61,13 +59,13 @@ class CaptchaValidator extends Validator public function getCaptchaAction() { if (strpos($this->captchaAction, '/') !== false) { // contains controller or module - $ca = \Yii::$application->createController($this->captchaAction); + $ca = \Yii::$app->createController($this->captchaAction); if ($ca !== null) { list($controller, $actionID) = $ca; $action = $controller->createAction($actionID); } } else { - $action = \Yii::$application->getController()->createAction($this->captchaAction); + $action = \Yii::$app->getController()->createAction($this->captchaAction); } if ($action === null) { @@ -85,7 +83,7 @@ class CaptchaValidator extends Validator public function clientValidateAttribute($object, $attribute) { $captcha = $this->getCaptchaAction(); - $message = $this->message !== null ? $this->message : \Yii::t('yii', 'The verification code is incorrect.'); + $message = $this->message !== null ? $this->message : \Yii::t('yii|The verification code is incorrect.'); $message = strtr($message, array( '{attribute}' => $object->getAttributeLabel($attribute), '{value}' => $object->$attribute, diff --git a/framework/validators/CompareValidator.php b/framework/validators/CompareValidator.php index 9345b73..43f2edf 100644 --- a/framework/validators/CompareValidator.php +++ b/framework/validators/CompareValidator.php @@ -1,9 +1,7 @@ <?php /** - * CompareValidator class file. - * * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ @@ -96,37 +94,37 @@ class CompareValidator extends Validator case '=': case '==': if (($this->strict && $value !== $compareValue) || (!$this->strict && $value != $compareValue)) { - $message = ($this->message !== null) ? $this->message : Yii::t('yii', '{attribute} must be repeated exactly.'); + $message = ($this->message !== null) ? $this->message : Yii::t('yii|{attribute} must be repeated exactly.'); $this->addError($object, $attribute, $message, array('{compareAttribute}' => $compareLabel)); } break; case '!=': if (($this->strict && $value === $compareValue) || (!$this->strict && $value == $compareValue)) { - $message = ($this->message !== null) ? $this->message : Yii::t('yii', '{attribute} must not be equal to "{compareValue}".'); + $message = ($this->message !== null) ? $this->message : Yii::t('yii|{attribute} must not be equal to "{compareValue}".'); $this->addError($object, $attribute, $message, array('{compareAttribute}' => $compareLabel, '{compareValue}' => $compareValue)); } break; case '>': if ($value <= $compareValue) { - $message = ($this->message !== null) ? $this->message : Yii::t('yii', '{attribute} must be greater than "{compareValue}".'); + $message = ($this->message !== null) ? $this->message : Yii::t('yii|{attribute} must be greater than "{compareValue}".'); $this->addError($object, $attribute, $message, array('{compareAttribute}' => $compareLabel, '{compareValue}' => $compareValue)); } break; case '>=': if ($value < $compareValue) { - $message = ($this->message !== null) ? $this->message : Yii::t('yii', '{attribute} must be greater than or equal to "{compareValue}".'); + $message = ($this->message !== null) ? $this->message : Yii::t('yii|{attribute} must be greater than or equal to "{compareValue}".'); $this->addError($object, $attribute, $message, array('{compareAttribute}' => $compareLabel, '{compareValue}' => $compareValue)); } break; case '<': if ($value >= $compareValue) { - $message = ($this->message !== null) ? $this->message : Yii::t('yii', '{attribute} must be less than "{compareValue}".'); + $message = ($this->message !== null) ? $this->message : Yii::t('yii|{attribute} must be less than "{compareValue}".'); $this->addError($object, $attribute, $message, array('{compareAttribute}' => $compareLabel, '{compareValue}' => $compareValue)); } break; case '<=': if ($value > $compareValue) { - $message = ($this->message !== null) ? $this->message : Yii::t('yii', '{attribute} must be less than or equal to "{compareValue}".'); + $message = ($this->message !== null) ? $this->message : Yii::t('yii|{attribute} must be less than or equal to "{compareValue}".'); $this->addError($object, $attribute, $message, array('{compareAttribute}' => $compareLabel, '{compareValue}' => $compareValue)); } break; @@ -158,37 +156,37 @@ class CompareValidator extends Validator case '=': case '==': if ($message === null) { - $message = Yii::t('yii', '{attribute} must be repeated exactly.'); + $message = Yii::t('yii|{attribute} must be repeated exactly.'); } $condition = 'value!=' . $compareValue; break; case '!=': if ($message === null) { - $message = Yii::t('yii', '{attribute} must not be equal to "{compareValue}".'); + $message = Yii::t('yii|{attribute} must not be equal to "{compareValue}".'); } $condition = 'value==' . $compareValue; break; case '>': if ($message === null) { - $message = Yii::t('yii', '{attribute} must be greater than "{compareValue}".'); + $message = Yii::t('yii|{attribute} must be greater than "{compareValue}".'); } $condition = 'value<=' . $compareValue; break; case '>=': if ($message === null) { - $message = Yii::t('yii', '{attribute} must be greater than or equal to "{compareValue}".'); + $message = Yii::t('yii|{attribute} must be greater than or equal to "{compareValue}".'); } $condition = 'value<' . $compareValue; break; case '<': if ($message === null) { - $message = Yii::t('yii', '{attribute} must be less than "{compareValue}".'); + $message = Yii::t('yii|{attribute} must be less than "{compareValue}".'); } $condition = 'value>=' . $compareValue; break; case '<=': if ($message === null) { - $message = Yii::t('yii', '{attribute} must be less than or equal to "{compareValue}".'); + $message = Yii::t('yii|{attribute} must be less than or equal to "{compareValue}".'); } $condition = 'value>' . $compareValue; break; diff --git a/framework/validators/DateValidator.php b/framework/validators/DateValidator.php index f4fa866..7899c95 100644 --- a/framework/validators/DateValidator.php +++ b/framework/validators/DateValidator.php @@ -1,9 +1,7 @@ <?php /** - * DateValidator class file. - * * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ @@ -66,7 +64,7 @@ class DateValidator extends Validator } if (!$valid) { - $message = ($this->message !== null) ? $this->message : \Yii::t('yii', 'The format of {attribute} is invalid.'); + $message = ($this->message !== null) ? $this->message : \Yii::t('yii|The format of {attribute} is invalid.'); $this->addError($object, $attribute, $message); } } diff --git a/framework/validators/DefaultValueValidator.php b/framework/validators/DefaultValueValidator.php index 1673182..be06768 100644 --- a/framework/validators/DefaultValueValidator.php +++ b/framework/validators/DefaultValueValidator.php @@ -1,9 +1,7 @@ <?php /** - * DefaultValueValidator class file. - * * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ diff --git a/framework/validators/EmailValidator.php b/framework/validators/EmailValidator.php index 8fd8120..d1d2257 100644 --- a/framework/validators/EmailValidator.php +++ b/framework/validators/EmailValidator.php @@ -1,9 +1,7 @@ <?php /** - * EmailValidator class file. - * * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ @@ -63,7 +61,7 @@ class EmailValidator extends Validator return; } if (!$this->validateValue($value)) { - $message = ($this->message !== null) ? $this->message : \Yii::t('yii', '{attribute} is not a valid email address.'); + $message = ($this->message !== null) ? $this->message : \Yii::t('yii|{attribute} is not a valid email address.'); $this->addError($object, $attribute, $message); } } @@ -100,7 +98,7 @@ class EmailValidator extends Validator */ public function clientValidateAttribute($object, $attribute) { - $message = ($this->message !== null) ? $this->message : \Yii::t('yii', '{attribute} is not a valid email address.'); + $message = ($this->message !== null) ? $this->message : \Yii::t('yii|{attribute} is not a valid email address.'); $message = strtr($message, array( '{attribute}' => $object->getAttributeLabel($attribute), '{value}' => $object->$attribute, diff --git a/framework/validators/ExistValidator.php b/framework/validators/ExistValidator.php index be710bd..8df3e19 100644 --- a/framework/validators/ExistValidator.php +++ b/framework/validators/ExistValidator.php @@ -1,9 +1,7 @@ <?php /** - * ExistValidator class file. - * * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ @@ -68,7 +66,7 @@ class ExistValidator extends Validator $query = $className::find(); $query->where(array($column->name => $value)); if (!$query->exists()) { - $message = ($this->message !== null) ? $this->message : \Yii::t('yii', '{attribute} "{value}" is invalid.'); + $message = ($this->message !== null) ? $this->message : \Yii::t('yii|{attribute} "{value}" is invalid.'); $this->addError($object, $attribute, $message); } } diff --git a/framework/validators/FileValidator.php b/framework/validators/FileValidator.php index 106b9a6..b05ac2a 100644 --- a/framework/validators/FileValidator.php +++ b/framework/validators/FileValidator.php @@ -1,9 +1,7 @@ <?php /** - * CFileValidator class file. - * * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ @@ -115,7 +113,7 @@ class CFileValidator extends Validator return $this->emptyAttribute($object, $attribute); if (count($files) > $this->maxFiles) { - $message = $this->tooMany !== null ? $this->tooMany : \Yii::t('yii', '{attribute} cannot accept more than {limit} files.'); + $message = $this->tooMany !== null ? $this->tooMany : \Yii::t('yii|{attribute} cannot accept more than {limit} files.'); $this->addError($object, $attribute, $message, array('{attribute}' => $attribute, '{limit}' => $this->maxFiles)); } else foreach ($files as $file) @@ -145,20 +143,20 @@ class CFileValidator extends Validator return $this->emptyAttribute($object, $attribute); elseif ($error == UPLOAD_ERR_INI_SIZE || $error == UPLOAD_ERR_FORM_SIZE || $this->maxSize !== null && $file->getSize() > $this->maxSize) { - $message = $this->tooLarge !== null ? $this->tooLarge : \Yii::t('yii', 'The file "{file}" is too large. Its size cannot exceed {limit} bytes.'); + $message = $this->tooLarge !== null ? $this->tooLarge : \Yii::t('yii|The file "{file}" is too large. Its size cannot exceed {limit} bytes.'); $this->addError($object, $attribute, $message, array('{file}' => $file->getName(), '{limit}' => $this->getSizeLimit())); } elseif ($error == UPLOAD_ERR_PARTIAL) - throw new CException(\Yii::t('yii', 'The file "{file}" was only partially uploaded.', array('{file}' => $file->getName()))); + throw new CException(\Yii::t('yii|The file "{file}" was only partially uploaded.', array('{file}' => $file->getName()))); elseif ($error == UPLOAD_ERR_NO_TMP_DIR) - throw new CException(\Yii::t('yii', 'Missing the temporary folder to store the uploaded file "{file}".', array('{file}' => $file->getName()))); + throw new CException(\Yii::t('yii|Missing the temporary folder to store the uploaded file "{file}".', array('{file}' => $file->getName()))); elseif ($error == UPLOAD_ERR_CANT_WRITE) - throw new CException(\Yii::t('yii', 'Failed to write the uploaded file "{file}" to disk.', array('{file}' => $file->getName()))); + throw new CException(\Yii::t('yii|Failed to write the uploaded file "{file}" to disk.', array('{file}' => $file->getName()))); elseif (defined('UPLOAD_ERR_EXTENSION') && $error == UPLOAD_ERR_EXTENSION) // available for PHP 5.2.0 or above - throw new CException(\Yii::t('yii', 'File upload was stopped by extension.')); + throw new CException(\Yii::t('yii|File upload was stopped by extension.')); if ($this->minSize !== null && $file->getSize() < $this->minSize) { - $message = $this->tooSmall !== null ? $this->tooSmall : \Yii::t('yii', 'The file "{file}" is too small. Its size cannot be smaller than {limit} bytes.'); + $message = $this->tooSmall !== null ? $this->tooSmall : \Yii::t('yii|The file "{file}" is too small. Its size cannot be smaller than {limit} bytes.'); $this->addError($object, $attribute, $message, array('{file}' => $file->getName(), '{limit}' => $this->minSize)); } @@ -170,7 +168,7 @@ class CFileValidator extends Validator $types = $this->types; if (!in_array(strtolower($file->getExtensionName()), $types)) { - $message = $this->wrongType !== null ? $this->wrongType : \Yii::t('yii', 'The file "{file}" cannot be uploaded. Only files with these extensions are allowed: {extensions}.'); + $message = $this->wrongType !== null ? $this->wrongType : \Yii::t('yii|The file "{file}" cannot be uploaded. Only files with these extensions are allowed: {extensions}.'); $this->addError($object, $attribute, $message, array('{file}' => $file->getName(), '{extensions}' => implode(', ', $types))); } } @@ -185,7 +183,7 @@ class CFileValidator extends Validator { if (!$this->allowEmpty) { - $message = $this->message !== null ? $this->message : \Yii::t('yii', '{attribute} cannot be blank.'); + $message = $this->message !== null ? $this->message : \Yii::t('yii|{attribute} cannot be blank.'); $this->addError($object, $attribute, $message); } } diff --git a/framework/validators/FilterValidator.php b/framework/validators/FilterValidator.php index d20defd..c891979 100644 --- a/framework/validators/FilterValidator.php +++ b/framework/validators/FilterValidator.php @@ -1,9 +1,7 @@ <?php /** - * FilterValidator class file. - * * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ diff --git a/framework/validators/InlineValidator.php b/framework/validators/InlineValidator.php index e324b4b..5c12d52 100644 --- a/framework/validators/InlineValidator.php +++ b/framework/validators/InlineValidator.php @@ -1,9 +1,7 @@ <?php /** - * InlineValidator class file. - * * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ diff --git a/framework/validators/NumberValidator.php b/framework/validators/NumberValidator.php index 4596fc1..89363fb 100644 --- a/framework/validators/NumberValidator.php +++ b/framework/validators/NumberValidator.php @@ -1,9 +1,7 @@ <?php /** - * NumberValidator class file. - * * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ @@ -73,21 +71,21 @@ class NumberValidator extends Validator } if ($this->integerOnly) { if (!preg_match($this->integerPattern, "$value")) { - $message = $this->message !== null ? $this->message : Yii::t('yii', '{attribute} must be an integer.'); + $message = $this->message !== null ? $this->message : Yii::t('yii|{attribute} must be an integer.'); $this->addError($object, $attribute, $message); } } else { if (!preg_match($this->numberPattern, "$value")) { - $message = $this->message !== null ? $this->message : Yii::t('yii', '{attribute} must be a number.'); + $message = $this->message !== null ? $this->message : Yii::t('yii|{attribute} must be a number.'); $this->addError($object, $attribute, $message); } } if ($this->min !== null && $value < $this->min) { - $message = $this->tooSmall !== null ? $this->tooSmall : Yii::t('yii', '{attribute} is too small (minimum is {min}).'); + $message = $this->tooSmall !== null ? $this->tooSmall : Yii::t('yii|{attribute} is too small (minimum is {min}).'); $this->addError($object, $attribute, $message, array('{min}' => $this->min)); } if ($this->max !== null && $value > $this->max) { - $message = $this->tooBig !== null ? $this->tooBig : Yii::t('yii', '{attribute} is too big (maximum is {max}).'); + $message = $this->tooBig !== null ? $this->tooBig : Yii::t('yii|{attribute} is too big (maximum is {max}).'); $this->addError($object, $attribute, $message, array('{max}' => $this->max)); } } @@ -103,8 +101,8 @@ class NumberValidator extends Validator $label = $object->getAttributeLabel($attribute); if (($message = $this->message) === null) { - $message = $this->integerOnly ? Yii::t('yii', '{attribute} must be an integer.') - : Yii::t('yii', '{attribute} must be a number.'); + $message = $this->integerOnly ? Yii::t('yii|{attribute} must be an integer.') + : Yii::t('yii|{attribute} must be a number.'); } $message = strtr($message, array( '{attribute}' => $label, @@ -118,7 +116,7 @@ if(!value.match($pattern)) { "; if ($this->min !== null) { if (($tooSmall = $this->tooSmall) === null) { - $tooSmall = Yii::t('yii', '{attribute} is too small (minimum is {min}).'); + $tooSmall = Yii::t('yii|{attribute} is too small (minimum is {min}).'); } $tooSmall = strtr($tooSmall, array( '{attribute}' => $label, @@ -133,7 +131,7 @@ if(value<{$this->min}) { } if ($this->max !== null) { if (($tooBig = $this->tooBig) === null) { - $tooBig = Yii::t('yii', '{attribute} is too big (maximum is {max}).'); + $tooBig = Yii::t('yii|{attribute} is too big (maximum is {max}).'); } $tooBig = strtr($tooBig, array( '{attribute}' => $label, diff --git a/framework/validators/RangeValidator.php b/framework/validators/RangeValidator.php index b2ff773..e23567c 100644 --- a/framework/validators/RangeValidator.php +++ b/framework/validators/RangeValidator.php @@ -1,9 +1,7 @@ <?php /** - * RangeValidator class file. - * * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ @@ -58,10 +56,10 @@ class RangeValidator extends Validator throw new InvalidConfigException('The "range" property must be specified as an array.'); } if (!$this->not && !in_array($value, $this->range, $this->strict)) { - $message = ($this->message !== null) ? $this->message : \Yii::t('yii', '{attribute} should be in the list.'); + $message = ($this->message !== null) ? $this->message : \Yii::t('yii|{attribute} should be in the list.'); $this->addError($object, $attribute, $message); } elseif ($this->not && in_array($value, $this->range, $this->strict)) { - $message = ($this->message !== null) ? $this->message : \Yii::t('yii', '{attribute} should NOT be in the list.'); + $message = ($this->message !== null) ? $this->message : \Yii::t('yii|{attribute} should NOT be in the list.'); $this->addError($object, $attribute, $message); } } @@ -80,7 +78,7 @@ class RangeValidator extends Validator } if (($message = $this->message) === null) { - $message = $this->not ? \Yii::t('yii', '{attribute} should NOT be in the list.') : \Yii::t('yii', '{attribute} should be in the list.'); + $message = $this->not ? \Yii::t('yii|{attribute} should NOT be in the list.') : \Yii::t('yii|{attribute} should be in the list.'); } $message = strtr($message, array( '{attribute}' => $object->getAttributeLabel($attribute), diff --git a/framework/validators/RegularExpressionValidator.php b/framework/validators/RegularExpressionValidator.php index fbdb062..df2b657 100644 --- a/framework/validators/RegularExpressionValidator.php +++ b/framework/validators/RegularExpressionValidator.php @@ -1,9 +1,7 @@ <?php /** - * RegularExpressionValidator class file. - * * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ @@ -51,7 +49,7 @@ class RegularExpressionValidator extends Validator throw new \yii\base\Exception('The "pattern" property must be specified with a valid regular expression.'); } if ((!$this->not && !preg_match($this->pattern, $value)) || ($this->not && preg_match($this->pattern, $value))) { - $message = ($this->message !== null) ? $this->message : \Yii::t('yii', '{attribute} is invalid.'); + $message = ($this->message !== null) ? $this->message : \Yii::t('yii|{attribute} is invalid.'); $this->addError($object, $attribute, $message); } } @@ -69,7 +67,7 @@ class RegularExpressionValidator extends Validator throw new \yii\base\Exception('The "pattern" property must be specified with a valid regular expression.'); } - $message = ($this->message !== null) ? $this->message : \Yii::t('yii', '{attribute} is invalid.'); + $message = ($this->message !== null) ? $this->message : \Yii::t('yii|{attribute} is invalid.'); $message = strtr($message, array( '{attribute}' => $object->getAttributeLabel($attribute), '{value}' => $object->$attribute, diff --git a/framework/validators/RequiredValidator.php b/framework/validators/RequiredValidator.php index f0f4bfd..66b9c3c 100644 --- a/framework/validators/RequiredValidator.php +++ b/framework/validators/RequiredValidator.php @@ -1,9 +1,7 @@ <?php /** - * RequiredValidator class file. - * * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ @@ -47,12 +45,12 @@ class RequiredValidator extends Validator $value = $object->$attribute; if ($this->requiredValue === null) { if ($this->strict && $value === null || !$this->strict && $this->isEmpty($value, true)) { - $message = ($this->message !== null) ? $this->message : \Yii::t('yii', '{attribute} cannot be blank.'); + $message = ($this->message !== null) ? $this->message : \Yii::t('yii|{attribute} cannot be blank.'); $this->addError($object, $attribute, $message); } } else { if (!$this->strict && $value != $this->requiredValue || $this->strict && $value !== $this->requiredValue) { - $message = ($this->message !== null) ? $this->message : \Yii::t('yii', '{attribute} must be "{requiredValue}".'); + $message = ($this->message !== null) ? $this->message : \Yii::t('yii|{attribute} must be "{requiredValue}".'); $this->addError($object, $attribute, $message, array( '{requiredValue}' => $this->requiredValue, )); @@ -71,7 +69,7 @@ class RequiredValidator extends Validator $message = $this->message; if ($this->requiredValue !== null) { if ($message === null) { - $message = \Yii::t('yii', '{attribute} must be "{requiredValue}".'); + $message = \Yii::t('yii|{attribute} must be "{requiredValue}".'); } $message = strtr($message, array( '{attribute}' => $object->getAttributeLabel($attribute), @@ -85,7 +83,7 @@ if (value != " . json_encode($this->requiredValue) . ") { "; } else { if ($message === null) { - $message = \Yii::t('yii', '{attribute} cannot be blank.'); + $message = \Yii::t('yii|{attribute} cannot be blank.'); } $message = strtr($message, array( '{attribute}' => $object->getAttributeLabel($attribute), diff --git a/framework/validators/StringValidator.php b/framework/validators/StringValidator.php index fe69a4d..9135b9e 100644 --- a/framework/validators/StringValidator.php +++ b/framework/validators/StringValidator.php @@ -1,9 +1,7 @@ <?php /** - * StringValidator class file. - * * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ @@ -76,27 +74,27 @@ class StringValidator extends Validator } if (!is_string($value)) { - $message = ($this->message !== null) ? $this->message : \Yii::t('yii', '{attribute} must be a string.'); + $message = ($this->message !== null) ? $this->message : \Yii::t('yii|{attribute} must be a string.'); $this->addError($object, $attribute, $message); return; } if (function_exists('mb_strlen') && $this->encoding !== false) { - $length = mb_strlen($value, $this->encoding ? $this->encoding : \Yii::$application->charset); + $length = mb_strlen($value, $this->encoding ? $this->encoding : \Yii::$app->charset); } else { $length = strlen($value); } if ($this->min !== null && $length < $this->min) { - $message = ($this->tooShort !== null) ? $this->tooShort : \Yii::t('yii', '{attribute} is too short (minimum is {min} characters).'); + $message = ($this->tooShort !== null) ? $this->tooShort : \Yii::t('yii|{attribute} is too short (minimum is {min} characters).'); $this->addError($object, $attribute, $message, array('{min}' => $this->min)); } if ($this->max !== null && $length > $this->max) { - $message = ($this->tooLong !== null) ? $this->tooLong : \Yii::t('yii', '{attribute} is too long (maximum is {max} characters).'); + $message = ($this->tooLong !== null) ? $this->tooLong : \Yii::t('yii|{attribute} is too long (maximum is {max} characters).'); $this->addError($object, $attribute, $message, array('{max}' => $this->max)); } if ($this->is !== null && $length !== $this->is) { - $message = ($this->notEqual !== null) ? $this->notEqual : \Yii::t('yii', '{attribute} is of the wrong length (should be {length} characters).'); + $message = ($this->notEqual !== null) ? $this->notEqual : \Yii::t('yii|{attribute} is of the wrong length (should be {length} characters).'); $this->addError($object, $attribute, $message, array('{length}' => $this->is)); } } @@ -113,7 +111,7 @@ class StringValidator extends Validator $value = $object->$attribute; if (($notEqual = $this->notEqual) === null) { - $notEqual = \Yii::t('yii', '{attribute} is of the wrong length (should be {length} characters).'); + $notEqual = \Yii::t('yii|{attribute} is of the wrong length (should be {length} characters).'); } $notEqual = strtr($notEqual, array( '{attribute}' => $label, @@ -122,7 +120,7 @@ class StringValidator extends Validator )); if (($tooShort = $this->tooShort) === null) { - $tooShort = \Yii::t('yii', '{attribute} is too short (minimum is {min} characters).'); + $tooShort = \Yii::t('yii|{attribute} is too short (minimum is {min} characters).'); } $tooShort = strtr($tooShort, array( '{attribute}' => $label, @@ -131,7 +129,7 @@ class StringValidator extends Validator )); if (($tooLong = $this->tooLong) === null) { - $tooLong = \Yii::t('yii', '{attribute} is too long (maximum is {max} characters).'); + $tooLong = \Yii::t('yii|{attribute} is too long (maximum is {max} characters).'); } $tooLong = strtr($tooLong, array( '{attribute}' => $label, diff --git a/framework/validators/UniqueValidator.php b/framework/validators/UniqueValidator.php index 5d5e603..bc12f5a 100644 --- a/framework/validators/UniqueValidator.php +++ b/framework/validators/UniqueValidator.php @@ -1,9 +1,7 @@ <?php /** - * UniqueValidator class file. - * * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ @@ -86,7 +84,7 @@ class UniqueValidator extends Validator } if ($exists) { - $message = $this->message !== null ? $this->message : \Yii::t('yii', '{attribute} "{value}" has already been taken.'); + $message = $this->message !== null ? $this->message : \Yii::t('yii|{attribute} "{value}" has already been taken.'); $this->addError($object, $attribute, $message); } } diff --git a/framework/validators/UrlValidator.php b/framework/validators/UrlValidator.php index c6242a2..0ba039b 100644 --- a/framework/validators/UrlValidator.php +++ b/framework/validators/UrlValidator.php @@ -1,9 +1,7 @@ <?php /** - * UrlValidator class file. - * * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ @@ -55,7 +53,7 @@ class UrlValidator extends Validator if (($value = $this->validateValue($value)) !== false) { $object->$attribute = $value; } else { - $message = ($this->message !== null) ? $this->message : \Yii::t('yii', '{attribute} is not a valid URL.'); + $message = ($this->message !== null) ? $this->message : \Yii::t('yii|{attribute} is not a valid URL.'); $this->addError($object, $attribute, $message); } } @@ -97,7 +95,7 @@ class UrlValidator extends Validator */ public function clientValidateAttribute($object, $attribute) { - $message = ($this->message !== null) ? $this->message : \Yii::t('yii', '{attribute} is not a valid URL.'); + $message = ($this->message !== null) ? $this->message : \Yii::t('yii|{attribute} is not a valid URL.'); $message = strtr($message, array( '{attribute}' => $object->getAttributeLabel($attribute), '{value}' => $object->$attribute, diff --git a/framework/validators/Validator.php b/framework/validators/Validator.php index a03da7a..b688f32 100644 --- a/framework/validators/Validator.php +++ b/framework/validators/Validator.php @@ -1,9 +1,7 @@ <?php /** - * Validator class file. - * * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ diff --git a/framework/views/error.php b/framework/views/error.php index dbbc848..893640a 100644 --- a/framework/views/error.php +++ b/framework/views/error.php @@ -1,15 +1,16 @@ <?php /** * @var \Exception $exception - * @var \yii\base\ErrorHandler $owner + * @var \yii\base\ErrorHandler $context */ -$owner = $this->owner; +$context = $this->context; +$title = $context->htmlEncode($exception instanceof \yii\base\Exception || $exception instanceof \yii\base\ErrorException ? $exception->getName() : get_class($exception)); ?> <!DOCTYPE html> <html> <head> <meta charset="utf-8" /> - <title><?php echo get_class($exception)?></title> + <title><?php echo $title?></title> <style> body { @@ -50,8 +51,8 @@ $owner = $this->owner; </head> <body> - <h1><?php echo get_class($exception)?></h1> - <h2><?php echo nl2br($owner->htmlEncode($exception->getMessage()))?> </h2> + <h1><?php echo $title?></h1> + <h2><?php echo nl2br($context->htmlEncode($exception->getMessage()))?></h2> <p> The above error occurred while the Web server was processing your request. </p> @@ -60,7 +61,7 @@ $owner = $this->owner; </p> <div class="version"> <?php echo date('Y-m-d H:i:s', time())?> - <?php echo YII_DEBUG ? $owner->versionInfo : ''?> + <?php echo YII_DEBUG ? $context->versionInfo : ''?> </div> </body> </html> \ No newline at end of file diff --git a/framework/views/exception.php b/framework/views/exception.php index 8e8e905..db29302 100644 --- a/framework/views/exception.php +++ b/framework/views/exception.php @@ -4,12 +4,13 @@ * @var \yii\base\ErrorHandler $context */ $context = $this->context; +$title = $context->htmlEncode($exception instanceof \yii\base\Exception || $exception instanceof \yii\base\ErrorException ? $exception->getName().' ('.get_class($exception).')' : get_class($exception)); ?> <!DOCTYPE html> <html> <head> <meta charset="utf-8" /> - <title><?php echo get_class($exception)?></title> + <title><?php echo $title?></title> <style> html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,font,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td{border:0;outline:0;font-size:100%;vertical-align:baseline;background:transparent;margin:0;padding:0;} body{line-height:1;} @@ -160,7 +161,7 @@ $context = $this->context; <body> <div class="container"> - <h1><?php echo get_class($exception)?></h1> + <h1><?php echo $title?></h1> <p class="message"> <?php echo nl2br($context->htmlEncode($exception->getMessage()))?> @@ -182,7 +183,7 @@ $context = $this->context; <div class="version"> <?php echo date('Y-m-d H:i:s', time())?> - <?php echo YII_DEBUG ? $context->versionInfo : ''?> + <?php echo YII_DEBUG ? $context->getVersionInfo() : ''?> </div> </div> diff --git a/framework/views/migration.php b/framework/views/migration.php new file mode 100644 index 0000000..a75e22f --- /dev/null +++ b/framework/views/migration.php @@ -0,0 +1,23 @@ +<?php +/** + * This view is used by console/controllers/MigrateController.php + * The following variables are available in this view: + * + * @var string $className the new migration class name + */ +echo "<?php\n"; +?> + +class <?php echo $className; ?> extends \yii\db\Migration +{ + public function up() + { + + } + + public function down() + { + echo "<?php echo $className; ?> cannot be reverted.\n"; + return false; + } +} 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 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\web; + +use Yii; +use yii\base\Action; +use yii\base\ActionFilter; +use yii\base\HttpException; + +/** + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @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..3f8c057 --- /dev/null +++ b/framework/web/AccessRule.php @@ -0,0 +1,188 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\web; + +use yii\base\Component; +use yii\base\Action; +use yii\base\Controller; +use yii\web\User; +use yii\web\Request; + +/** + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @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 roles that this rule applies to. Two special roles are recognized, and + * they are checked via [[User::isGuest]]: + * + * - `?`: matches a guest user (not authenticated yet) + * - `@`: matches an authenticated user + * + * Using additional role names requires RBAC (Role-Based Access Control), and + * [[User::hasAccess()]] will be called. + * + * If this property is 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->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 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 ($role === '?' && $user->getIsGuest()) { + return true; + } elseif ($role === '@' && !$user->getIsGuest()) { + return true; + } elseif ($user->hasAccess($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 a25df7a..2533f04 100644 --- a/framework/web/Application.php +++ b/framework/web/Application.php @@ -1,14 +1,14 @@ <?php /** - * Application class file. - * * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ namespace yii\web; +use yii\base\InvalidParamException; + /** * Application is the base class for all application classes. * @@ -18,12 +18,17 @@ namespace yii\web; class Application extends \yii\base\Application { /** + * @var string the default route of this application. Defaults to 'site'. + */ + public $defaultRoute = 'site'; + + /** * Sets default path aliases. */ public function registerDefaultAliases() { parent::registerDefaultAliases(); - \Yii::$aliases['@www'] = dirname($_SERVER['SCRIPT_FILENAME']); + \Yii::$aliases['@webroot'] = dirname($_SERVER['SCRIPT_FILENAME']); } /** @@ -32,13 +37,44 @@ class Application extends \yii\base\Application */ public function processRequest() { - $route = $this->resolveRequest(); - return $this->runController($route, null); + list ($route, $params) = $this->getRequest()->resolve(); + return $this->runAction($route, $params); + } + + /** + * Returns the request component. + * @return Request the request component + */ + public function getRequest() + { + return $this->getComponent('request'); + } + + /** + * Returns the response component. + * @return Response the response component + */ + public function getResponse() + { + return $this->getComponent('response'); + } + + /** + * Returns the session component. + * @return Session the session component + */ + public function getSession() + { + return $this->getComponent('session'); } - protected function resolveRequest() + /** + * Returns the user component. + * @return User the user component + */ + public function getUser() { - return array(); + return $this->getComponent('user'); } /** @@ -55,6 +91,12 @@ class Application extends \yii\base\Application 'response' => array( 'class' => 'yii\web\Response', ), + 'session' => array( + 'class' => 'yii\web\Session', + ), + 'user' => array( + 'class' => 'yii\web\User', + ), )); } } diff --git a/framework/web/AssetManager.php b/framework/web/AssetManager.php index 2028a1c..60f4c07 100644 --- a/framework/web/AssetManager.php +++ b/framework/web/AssetManager.php @@ -1,7 +1,5 @@ <?php /** - * CAssetManager class file. - * * @author Qiang Xue <qiang.xue@gmail.com> * @link http://www.yiiframework.com/ * @copyright Copyright © 2008-2011 Yii Software LLC @@ -97,7 +95,7 @@ class CAssetManager extends CApplicationComponent { if($this->_basePath===null) { - $request=\Yii::$application->getRequest(); + $request=\Yii::$app->getRequest(); $this->setBasePath(dirname($request->getScriptFile()).DIRECTORY_SEPARATOR.self::DEFAULT_BASEPATH); } return $this->_basePath; @@ -113,7 +111,7 @@ class CAssetManager extends CApplicationComponent if(($basePath=realpath($value))!==false && is_dir($basePath) && is_writable($basePath)) $this->_basePath=$basePath; else - throw new CException(Yii::t('yii','CAssetManager.basePath "{path}" is invalid. Please make sure the directory exists and is writable by the Web server process.', + throw new CException(Yii::t('yii|CAssetManager.basePath "{path}" is invalid. Please make sure the directory exists and is writable by the Web server process.', array('{path}'=>$value))); } @@ -125,7 +123,7 @@ class CAssetManager extends CApplicationComponent { if($this->_baseUrl===null) { - $request=\Yii::$application->getRequest(); + $request=\Yii::$app->getRequest(); $this->setBaseUrl($request->getBaseUrl().'/'.self::DEFAULT_BASEPATH); } return $this->_baseUrl; @@ -236,7 +234,7 @@ class CAssetManager extends CApplicationComponent return $this->_published[$path]=$this->getBaseUrl().'/'.$dir; } } - throw new CException(Yii::t('yii','The asset "{asset}" to be published does not exist.', + throw new CException(Yii::t('yii|The asset "{asset}" to be published does not exist.', array('{asset}'=>$path))); } diff --git a/framework/web/CacheSession.php b/framework/web/CacheSession.php new file mode 100644 index 0000000..c125f01 --- /dev/null +++ b/framework/web/CacheSession.php @@ -0,0 +1,106 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\web; + +use Yii; +use yii\caching\Cache; +use yii\base\InvalidConfigException; + +/** + * CacheSession implements a session component using cache as storage medium. + * + * The cache being used can be any cache application component. + * The ID of the cache application component is specified via [[cache]], which defaults to 'cache'. + * + * Beware, by definition cache storage are volatile, which means the data stored on them + * may be swapped out and get lost. Therefore, you must make sure the cache used by this component + * is NOT volatile. If you want to use database as storage medium, use [[DbSession]] is a better choice. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class CacheSession extends Session +{ + /** + * @var Cache|string the cache object or the application component ID of the cache object. + * The session data will be stored using this cache object. + * + * After the CacheSession object is created, if you want to change this property, + * you should only assign it with a cache object. + */ + public $cache = 'cache'; + + /** + * Initializes the application component. + */ + public function init() + { + parent::init(); + if (is_string($this->cache)) { + $this->cache = Yii::$app->getComponent($this->cache); + } + if (!$this->cache instanceof Cache) { + throw new InvalidConfigException('CacheSession::cache must refer to the application component ID of a cache object.'); + } + } + + /** + * Returns a value indicating whether to use custom session storage. + * This method overrides the parent implementation and always returns true. + * @return boolean whether to use custom storage. + */ + public function getUseCustomStorage() + { + return true; + } + + /** + * Session read handler. + * Do not call this method directly. + * @param string $id session ID + * @return string the session data + */ + public function readSession($id) + { + $data = $this->cache->get($this->calculateKey($id)); + return $data === false ? '' : $data; + } + + /** + * Session write handler. + * Do not call this method directly. + * @param string $id session ID + * @param string $data session data + * @return boolean whether session write is successful + */ + public function writeSession($id, $data) + { + return $this->cache->set($this->calculateKey($id), $data, $this->getTimeout()); + } + + /** + * Session destroy handler. + * Do not call this method directly. + * @param string $id session ID + * @return boolean whether session is destroyed successfully + */ + public function destroySession($id) + { + return $this->cache->delete($this->calculateKey($id)); + } + + /** + * Generates a unique key used for storing session data in cache. + * @param string $id session variable name + * @return string a safe cache key associated with the session variable name + */ + protected function calculateKey($id) + { + return $this->cache->buildKey(array(__CLASS__, $id)); + } +} diff --git a/framework/web/Controller.php b/framework/web/Controller.php index 92722b5..93b74aa 100644 --- a/framework/web/Controller.php +++ b/framework/web/Controller.php @@ -1,17 +1,13 @@ <?php /** - * Controller class file. - * * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ namespace yii\web; -use yii\base\Action; -use yii\base\Exception; -use yii\base\HttpException; +use Yii; /** * Controller is the base class of Web controllers. @@ -22,54 +18,27 @@ use yii\base\HttpException; */ class Controller extends \yii\base\Controller { - private $_pageTitle; - /** - * Returns the request parameters that will be used for action parameter binding. - * Default implementation simply returns an empty array. - * Child classes may override this method to customize the parameters to be provided - * for action parameter binding (e.g. `$_GET`). - * @return array the request parameters (name-value pairs) to be used for action parameter binding + * Creates a URL using the given route and parameters. + * + * This method enhances [[UrlManager::createUrl()]] by supporting relative routes. + * A relative route is a route without a slash, such as "view". If the route is an empty + * string, [[route]] will be used; Otherwise, [[uniqueId]] will be prepended to a relative route. + * + * After this route conversion, the method This method calls [[UrlManager::createUrl()]] + * to create a URL. + * + * @param string $route the route. This can be either an absolute route or a relative route. + * @param array $params the parameters (name-value pairs) to be included in the generated URL + * @return string the created URL */ - public function getActionParams() + public function createUrl($route, $params = array()) { - return $_GET; - } - - /** - * This method is invoked when the request parameters do not satisfy the requirement of the specified action. - * The default implementation will throw an exception. - * @param Action $action the action being executed - * @param Exception $exception the exception about the invalid parameters - * @throws HttpException $exception a 400 HTTP exception - */ - public function invalidActionParams($action, $exception) - { - throw new HttpException(400, \Yii::t('yii', 'Your request is invalid.')); - } - - /** - * @return string the page title. Defaults to the controller name and the action name. - */ - public function getPageTitle() - { - if($this->_pageTitle !== null) { - return $this->_pageTitle; - } - else { - $name = ucfirst(basename($this->id)); - if($this->action!==null && strcasecmp($this->action->id,$this->defaultAction)) - return $this->_pageTitle=\Yii::$application->name.' - '.ucfirst($this->action->id).' '.$name; - else - return $this->_pageTitle=\Yii::$application->name.' - '.$name; + if (strpos($route, '/') === false) { + // a relative route + $route = $route === '' ? $this->getRoute() : $this->getUniqueId() . '/' . $route; } + return Yii::$app->getUrlManager()->createUrl($route, $params); } - /** - * @param string $value the page title. - */ - public function setPageTitle($value) - { - $this->_pageTitle = $value; - } } \ No newline at end of file diff --git a/framework/web/Cookie.php b/framework/web/Cookie.php index 40bab05..610e5aa 100644 --- a/framework/web/Cookie.php +++ b/framework/web/Cookie.php @@ -1,9 +1,7 @@ <?php /** - * Cookie class file. - * * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ @@ -47,5 +45,21 @@ class Cookie extends \yii\base\Object * By setting this property to true, the cookie will not be accessible by scripting languages, * such as JavaScript, which can effectively help to reduce identity theft through XSS attacks. */ - public $httpOnly = false; + public $httponly = false; + + /** + * Magic method to turn a cookie object into a string without having to explicitly access [[value]]. + * + * ~~~ + * if (isset($request->cookies['name'])) { + * $value = (string)$request->cookies['name']; + * } + * ~~~ + * + * @return string The value of the cookie. If the value property is null, an empty string will be returned. + */ + public function __toString() + { + return (string)$this->value; + } } diff --git a/framework/web/CookieCollection.php b/framework/web/CookieCollection.php index 1d87bf0..c76926b 100644 --- a/framework/web/CookieCollection.php +++ b/framework/web/CookieCollection.php @@ -1,37 +1,20 @@ <?php /** - * Dictionary class file. - * * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ namespace yii\web; +use Yii; use yii\base\DictionaryIterator; +use yii\helpers\SecurityHelper; /** - * Dictionary implements a collection that stores key-value pairs. - * - * You can access, add or remove an item with a key by using - * [[itemAt()]], [[add()]], and [[remove()]]. - * - * To get the number of the items in the dictionary, use [[getCount()]]. + * CookieCollection maintains the cookies available in the current request. * - * Because Dictionary implements a set of SPL interfaces, it can be used - * like a regular PHP array as follows, - * - * ~~~ - * $dictionary[$key] = $value; // add a key-value pair - * unset($dictionary[$key]); // remove the value with the specified key - * if (isset($dictionary[$key])) // if the dictionary contains the key - * foreach ($dictionary as $key=>$value) // traverse the items in the dictionary - * $n = count($dictionary); // returns the number of items in the dictionary - * ~~~ - * - * @property integer $count the number of items in the dictionary - * @property array $keys The keys in the dictionary + * @property integer $count the number of cookies in the collection * * @author Qiang Xue <qiang.xue@gmail.com> * @since 2.0 @@ -39,28 +22,35 @@ use yii\base\DictionaryIterator; class CookieCollection extends \yii\base\Object implements \IteratorAggregate, \ArrayAccess, \Countable { /** - * @var Cookie[] internal data storage + * @var boolean whether to enable cookie validation. By setting this property to true, + * if a cookie is tampered on the client side, it will be ignored when received on the server side. + */ + public $enableValidation = true; + /** + * @var string the secret key used for cookie validation. If not set, a random key will be generated and used. + */ + public $validationKey; + + /** + * @var Cookie[] the cookies in this collection (indexed by the cookie names) */ private $_cookies = array(); /** * Constructor. - * Initializes the dictionary with an array or an iterable object. - * @param array $cookies the initial data to be populated into the dictionary. - * This can be an array or an iterable object. * @param array $config name-value pairs that will be used to initialize the object properties */ - public function __construct($cookies = array(), $config = array()) + public function __construct($config = array()) { - $this->_cookies = $cookies; parent::__construct($config); + $this->_cookies = $this->loadCookies(); } /** - * Returns an iterator for traversing the items in the dictionary. + * Returns an iterator for traversing the cookies in the collection. * This method is required by the SPL interface `IteratorAggregate`. - * It will be implicitly called when you use `foreach` to traverse the dictionary. - * @return DictionaryIterator an iterator for traversing the items in the dictionary. + * It will be implicitly called when you use `foreach` to traverse the collection. + * @return DictionaryIterator an iterator for traversing the cookies in the collection. */ public function getIterator() { @@ -68,10 +58,10 @@ class CookieCollection extends \yii\base\Object implements \IteratorAggregate, \ } /** - * Returns the number of items in the dictionary. + * Returns the number of cookies in the collection. * This method is required by the SPL `Countable` interface. - * It will be implicitly called when you use `count($dictionary)`. - * @return integer number of items in the dictionary. + * It will be implicitly called when you use `count($collection)`. + * @return integer the number of cookies in the collection. */ public function count() { @@ -79,8 +69,8 @@ class CookieCollection extends \yii\base\Object implements \IteratorAggregate, \ } /** - * Returns the number of items in the dictionary. - * @return integer the number of items in the dictionary + * Returns the number of cookies in the collection. + * @return integer the number of cookies in the collection. */ public function getCount() { @@ -88,72 +78,85 @@ class CookieCollection extends \yii\base\Object implements \IteratorAggregate, \ } /** - * Returns the keys stored in the dictionary. - * @return array the key list + * Returns the cookie with the specified name. + * @param string $name the cookie name + * @return Cookie the cookie with the specified name. Null if the named cookie does not exist. + * @see getValue() */ - public function getNames() + public function get($name) { - return array_keys($this->_cookies); + return isset($this->_cookies[$name]) ? $this->_cookies[$name] : null; } /** - * Returns the item with the specified key. - * @param mixed $name the key - * @return Cookie the element with the specified key. - * Null if the key cannot be found in the dictionary. + * Returns the value of the named cookie. + * @param string $name the cookie name + * @param mixed $defaultValue the value that should be returned when the named cookie does not exist. + * @return mixed the value of the named cookie. + * @see get() */ - public function getCookie($name) + public function getValue($name, $defaultValue = null) { - return isset($this->_cookies[$name]) ? $this->_cookies[$name] : null; + return isset($this->_cookies[$name]) ? $this->_cookies[$name]->value : $defaultValue; } /** - * Adds an item into the dictionary. - * Note, if the specified key already exists, the old value will be overwritten. - * @param Cookie $cookie value - * @throws Exception if the dictionary is read-only + * Adds a cookie to the collection. + * If there is already a cookie with the same name in the collection, it will be removed first. + * @param Cookie $cookie the cookie to be added */ - public function add(Cookie $cookie) + public function add($cookie) { if (isset($this->_cookies[$cookie->name])) { - $this->remove($this->_cookies[$cookie->name]); + $c = $this->_cookies[$cookie->name]; + setcookie($c->name, '', 0, $c->path, $c->domain, $c->secure, $c->httponly); } - setcookie($cookie->name, $cookie->value, $cookie->expire, $cookie->path, $cookie->domain, $cookie->secure, $cookie->httpOnly); + + $value = $cookie->value; + if ($this->enableValidation) { + if ($this->validationKey === null) { + $key = SecurityHelper::getSecretKey(__CLASS__ . '/' . Yii::$app->id); + } else { + $key = $this->validationKey; + } + $value = SecurityHelper::hashData(serialize($value), $key); + } + + setcookie($cookie->name, $value, $cookie->expire, $cookie->path, $cookie->domain, $cookie->secure, $cookie->httponly); $this->_cookies[$cookie->name] = $cookie; } /** - * Removes an item from the dictionary by its key. - * @param mixed $key the key of the item to be removed - * @return mixed the removed value, null if no such key exists. - * @throws Exception if the dictionary is read-only + * Removes a cookie from the collection. + * @param Cookie|string $cookie the cookie object or the name of the cookie to be removed. */ - public function remove(Cookie $cookie) + public function remove($cookie) { - setcookie($cookie->name, '', 0, $cookie->path, $cookie->domain, $cookie->secure, $cookie->httpOnly); - unset($this->_cookies[$cookie->name]); + if (is_string($cookie) && isset($this->_cookies[$cookie])) { + $cookie = $this->_cookies[$cookie]; + } + if ($cookie instanceof Cookie) { + setcookie($cookie->name, '', 0, $cookie->path, $cookie->domain, $cookie->secure, $cookie->httponly); + unset($this->_cookies[$cookie->name]); + } } /** - * Removes all items from the dictionary. - * @param boolean $safeClear whether to clear every item by calling [[remove]]. - * Defaults to false, meaning all items in the dictionary will be cleared directly - * without calling [[remove]]. + * Removes all cookies. */ - public function clear($safeClear = false) + public function removeAll() { - if ($safeClear) { - foreach (array_keys($this->_cookies) as $key) { - $this->remove($key); - } - } else { - $this->_cookies = array(); + foreach ($this->_cookies as $cookie) { + setcookie($cookie->name, '', 0, $cookie->path, $cookie->domain, $cookie->secure, $cookie->httponly); } + $this->_cookies = array(); } /** - * Returns the dictionary as a PHP array. - * @return array the list of items in array + * Returns the collection as a PHP array. + * @return array the array representation of the collection. + * The array keys are cookie names, and the array values are the corresponding + * cookie objects. */ public function toArray() { @@ -161,76 +164,82 @@ class CookieCollection extends \yii\base\Object implements \IteratorAggregate, \ } /** - * Returns whether there is an element at the specified offset. + * Returns whether there is a cookie with the specified name. * This method is required by the SPL interface `ArrayAccess`. - * It is implicitly called when you use something like `isset($dictionary[$offset])`. - * This is equivalent to [[contains]]. - * @param mixed $offset the offset to check on - * @return boolean + * It is implicitly called when you use something like `isset($collection[$name])`. + * @param string $name the cookie name + * @return boolean whether the named cookie exists */ - public function offsetExists($offset) + public function offsetExists($name) { - return isset($this->_cookies[$offset]); + return isset($this->_cookies[$name]); } /** - * Returns the element at the specified offset. + * Returns the cookie with the specified name. * This method is required by the SPL interface `ArrayAccess`. - * It is implicitly called when you use something like `$value = $dictionary[$offset];`. - * This is equivalent to [[itemAt]]. - * @param mixed $offset the offset to retrieve element. - * @return mixed the element at the offset, null if no element is found at the offset + * It is implicitly called when you use something like `$cookie = $collection[$name];`. + * This is equivalent to [[get()]]. + * @param string $name the cookie name + * @return Cookie the cookie with the specified name, null if the named cookie does not exist. */ - public function offsetGet($offset) + public function offsetGet($name) { - return $this->getCookie($offset); + return $this->get($name); } /** - * Sets the element at the specified offset. + * Adds the cookie to the collection. * This method is required by the SPL interface `ArrayAccess`. - * It is implicitly called when you use something like `$dictionary[$offset] = $item;`. - * If the offset is null, the new item will be appended to the dictionary. - * Otherwise, the existing item at the offset will be replaced with the new item. - * This is equivalent to [[add]]. - * @param mixed $offset the offset to set element - * @param mixed $item the element value - */ - public function offsetSet($offset, $item) + * It is implicitly called when you use something like `$collection[$name] = $cookie;`. + * This is equivalent to [[add()]]. + * @param string $name the cookie name + * @param Cookie $cookie the cookie to be added + */ + public function offsetSet($name, $cookie) { - $this->add($item); + $this->add($cookie); } /** - * Unsets the element at the specified offset. + * Removes the named cookie. * This method is required by the SPL interface `ArrayAccess`. - * It is implicitly called when you use something like `unset($dictionary[$offset])`. - * This is equivalent to [[remove]]. - * @param mixed $offset the offset to unset element + * It is implicitly called when you use something like `unset($collection[$name])`. + * This is equivalent to [[remove()]]. + * @param string $name the cookie name */ - public function offsetUnset($offset) + public function offsetUnset($name) { - if (isset($this->_cookies[$offset])) { - $this->remove($this->_cookies[$offset]); - } + $this->remove($name); } /** - * @return array list of validated cookies + * Returns the current cookies in terms of [[Cookie]] objects. + * @return Cookie[] list of current cookies */ - protected function loadCookies($data) + protected function loadCookies() { $cookies = array(); - if ($this->_request->enableCookieValidation) { - $sm = Yii::app()->getSecurityManager(); + if ($this->enableValidation) { + if ($this->validationKey === null) { + $key = SecurityHelper::getSecretKey(__CLASS__ . '/' . Yii::$app->id); + } else { + $key = $this->validationKey; + } foreach ($_COOKIE as $name => $value) { - if (is_string($value) && ($value = $sm->validateData($value)) !== false) { - $cookies[$name] = new CHttpCookie($name, @unserialize($value)); + if (is_string($value) && ($value = SecurityHelper::validateData($value, $key)) !== false) { + $cookies[$name] = new Cookie(array( + 'name' => $name, + 'value' => @unserialize($value), + )); } } } else { foreach ($_COOKIE as $name => $value) { - $cookies[$name] = new CHttpCookie($name, $value); + $cookies[$name] = new Cookie(array( + 'name' => $name, + 'value' => $value, + )); } } return $cookies; diff --git a/framework/web/DbSession.php b/framework/web/DbSession.php new file mode 100644 index 0000000..d3afc76 --- /dev/null +++ b/framework/web/DbSession.php @@ -0,0 +1,221 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\web; + +use Yii; +use yii\db\Connection; +use yii\db\Query; +use yii\base\InvalidConfigException; + +/** + * DbSession extends [[Session]] by using database as session data storage. + * + * By default, DbSession stores session data in a DB table named 'tbl_session'. This table + * must be pre-created. The table name can be changed by setting [[sessionTable]]. + * + * The following example shows how you can configure the application to use DbSession: + * + * ~~~ + * 'session' => array( + * 'class' => 'yii\web\DbSession', + * // 'db' => 'mydb', + * // 'sessionTable' => 'my_session', + * ) + * ~~~ + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class DbSession extends Session +{ + /** + * @var Connection|string the DB connection object or the application component ID of the DB connection. + * After the DbSession object is created, if you want to change this property, you should only assign it + * with a DB connection object. + */ + public $db = 'db'; + /** + * @var string the name of the DB table that stores the session data. + * The table should be pre-created as follows: + * + * ~~~ + * CREATE TABLE tbl_session + * ( + * id CHAR(40) NOT NULL PRIMARY KEY, + * expire INTEGER, + * data BLOB + * ) + * ~~~ + * + * where 'BLOB' refers to the BLOB-type of your preferred DBMS. Below are the BLOB type + * that can be used for some popular DBMS: + * + * - MySQL: LONGBLOB + * - PostgreSQL: BYTEA + * - MSSQL: BLOB + * + * When using DbSession in a production server, we recommend you create a DB index for the 'expire' + * column in the session table to improve the performance. + */ + public $sessionTable = 'tbl_session'; + + /** + * Initializes the DbSession component. + * This method will initialize the [[db]] property to make sure it refers to a valid DB connection. + * @throws InvalidConfigException if [[db]] is invalid. + */ + public function init() + { + parent::init(); + if (is_string($this->db)) { + $this->db = Yii::$app->getComponent($this->db); + } + if (!$this->db instanceof Connection) { + throw new InvalidConfigException("DbSession::db must be either a DB connection instance or the application component ID of a DB connection."); + } + } + + /** + * Returns a value indicating whether to use custom session storage. + * This method overrides the parent implementation and always returns true. + * @return boolean whether to use custom storage. + */ + public function getUseCustomStorage() + { + return true; + } + + /** + * Updates the current session ID with a newly generated one . + * Please refer to [[http://php.net/session_regenerate_id]] for more details. + * @param boolean $deleteOldSession Whether to delete the old associated session file or not. + */ + public function regenerateID($deleteOldSession = false) + { + $oldID = session_id(); + + // if no session is started, there is nothing to regenerate + if (empty($oldID)) { + return; + } + + parent::regenerateID(false); + $newID = session_id(); + + $query = new Query; + $row = $query->from($this->sessionTable) + ->where(array('id' => $oldID)) + ->createCommand($this->db) + ->queryRow(); + if ($row !== false) { + if ($deleteOldSession) { + $this->db->createCommand() + ->update($this->sessionTable, array('id' => $newID), array('id' => $oldID)) + ->execute(); + } else { + $row['id'] = $newID; + $this->db->createCommand() + ->insert($this->sessionTable, $row) + ->execute(); + } + } else { + // shouldn't reach here normally + $this->db->createCommand() + ->insert($this->sessionTable, array( + 'id' => $newID, + 'expire' => time() + $this->getTimeout(), + ))->execute(); + } + } + + /** + * Session read handler. + * Do not call this method directly. + * @param string $id session ID + * @return string the session data + */ + public function readSession($id) + { + $query = new Query; + $data = $query->select(array('data')) + ->from($this->sessionTable) + ->where('expire>:expire AND id=:id', array(':expire' => time(), ':id' => $id)) + ->createCommand($this->db) + ->queryScalar(); + return $data === false ? '' : $data; + } + + /** + * Session write handler. + * Do not call this method directly. + * @param string $id session ID + * @param string $data session data + * @return boolean whether session write is successful + */ + public function writeSession($id, $data) + { + // exception must be caught in session write handler + // http://us.php.net/manual/en/function.session-set-save-handler.php + try { + $expire = time() + $this->getTimeout(); + $query = new Query; + $exists = $query->select(array('id')) + ->from($this->sessionTable) + ->where(array('id' => $id)) + ->createCommand($this->db) + ->queryScalar(); + if ($exists === false) { + $this->db->createCommand() + ->insert($this->sessionTable, array( + 'id' => $id, + 'data' => $data, + 'expire' => $expire, + ))->execute(); + } else { + $this->db->createCommand() + ->update($this->sessionTable, array('data' => $data, 'expire' => $expire), array('id' => $id)) + ->execute(); + } + } catch (\Exception $e) { + if (YII_DEBUG) { + echo $e->getMessage(); + } + // it is too late to log an error message here + return false; + } + return true; + } + + /** + * Session destroy handler. + * Do not call this method directly. + * @param string $id session ID + * @return boolean whether session is destroyed successfully + */ + public function destroySession($id) + { + $this->db->createCommand() + ->delete($this->sessionTable, array('id' => $id)) + ->execute(); + return true; + } + + /** + * Session GC (garbage collection) handler. + * Do not call this method directly. + * @param integer $maxLifetime the number of seconds after which data will be seen as 'garbage' and cleaned up. + * @return boolean whether session is GCed successfully + */ + public function gcSession($maxLifetime) + { + $this->db->createCommand() + ->delete($this->sessionTable, 'expire<:expire', array(':expire' => time())) + ->execute(); + return true; + } +} diff --git a/framework/web/HttpCache.php b/framework/web/HttpCache.php new file mode 100644 index 0000000..f64b37f --- /dev/null +++ b/framework/web/HttpCache.php @@ -0,0 +1,131 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\web; + +use Yii; +use yii\base\ActionFilter; +use yii\base\Action; + +/** + * @author Da:Sourcerer <webmaster@dasourcerer.net> + * @author Qiang Xue <qiang.xue@gmail.com> + * @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]]. The callback should return a UNIX timestamp. + */ + 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 serving + * as the seed for generating an Etag. + */ + public $etagSeed; + /** + * @var mixed additional parameters that should be passed to the [[lastModified]] and [[etagSeed]] callbacks. + */ + public $params; + /** + * @var string HTTP cache control header. If null, the header will not be sent. + */ + public $cacheControlHeader = 'Cache-Control: 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) + { + $verb = Yii::$app->request->getRequestMethod(); + if ($verb !== 'GET' && $verb !== 'HEAD' || $this->lastModified === null && $this->etagSeed === null) { + 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); + } + + $this->sendCacheControlHeader(); + if ($etag !== null) { + header("ETag: $etag"); + } + + if ($this->validateCache($lastModified, $etag)) { + header('HTTP/1.1 304 Not Modified'); + return false; + } + + if ($lastModified !== null) { + header('Last-Modified: ' . gmdate('D, d M Y H:i:s', $lastModified) . ' GMT'); + } + return true; + } + + /** + * Validates if the HTTP cache contains valid content. + * @param integer $lastModified the calculated Last-Modified value in terms of a UNIX timestamp. + * If null, the Last-Modified header will not be validated. + * @param string $etag the calculated ETag value. If null, the ETag header will not be validated. + * @return boolean whether the HTTP cache is still valid. + */ + protected function validateCache($lastModified, $etag) + { + if ($lastModified !== null && (!isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) || @strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) < $lastModified)) { + return false; + } else { + return $etag === null || isset($_SERVER['HTTP_IF_NONE_MATCH']) && $_SERVER['HTTP_IF_NONE_MATCH'] === $etag; + } + } + + /** + * Sends the cache control header to the client + * @see cacheControl + */ + protected function sendCacheControlHeader() + { + session_cache_limiter('public'); + header('Pragma:', true); + if ($this->cacheControlHeader !== null) { + header($this->cacheControlHeader, true); + } + } + + /** + * Generates an Etag from the 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/Identity.php b/framework/web/Identity.php new file mode 100644 index 0000000..4668337 --- /dev/null +++ b/framework/web/Identity.php @@ -0,0 +1,45 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\web; + +/** + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +interface Identity +{ + /** + * Returns an ID that can uniquely identify a user identity. + * The returned ID can be a string, an integer, or any serializable data. + * @return mixed an ID that uniquely identifies a user identity. + */ + public function getId(); + /** + * Returns a key that can be used to check the validity of a given identity ID. + * The space of such keys should be big and random enough to defeat potential identity attacks. + * The returned key can be a string, an integer, or any serializable data. + * @return mixed a key that is used to check the validity of a given identity ID. + * @see validateAuthKey() + */ + public function getAuthKey(); + /** + * Validates the given auth key. + * @param string $authKey the given auth key + * @return boolean whether the given auth key is valid. + * @see getAuthKey() + */ + public function validateAuthKey($authKey); + /** + * Finds an identity by the given ID. + * @param mixed $id the ID to be looked for + * @return Identity the identity object that matches the given ID. + * Null should be returned if such an identity cannot be found. + */ + public static function findIdentity($id); +} \ No newline at end of file diff --git a/framework/web/PageCache.php b/framework/web/PageCache.php new file mode 100644 index 0000000..29c8cc8 --- /dev/null +++ b/framework/web/PageCache.php @@ -0,0 +1,109 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\web; + +use Yii; +use yii\base\ActionFilter; +use yii\base\Action; +use yii\base\View; +use yii\caching\Dependency; + +/** + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class PageCache extends ActionFilter +{ + /** + * @var boolean whether the content being cached should be differentiated according to the route. + * A route consists of the requested controller ID and action ID. Defaults to true. + */ + public $varyByRoute = true; + /** + * @var View the view object that is used to create the fragment cache widget to implement page caching. + * If not set, the view registered with the application will be used. + */ + public $view; + /** + * @var string the application component ID of the [[\yii\caching\Cache|cache]] object. + */ + public $cache = 'cache'; + /** + * @var integer number of seconds that the data can remain valid in cache. + * Use 0 to indicate that the cached data will never expire. + */ + public $duration = 60; + /** + * @var array|Dependency the dependency that the cached content depends on. + * This can be either a [[Dependency]] object or a configuration array for creating the dependency object. + * For example, + * + * ~~~ + * array( + * 'class' => 'yii\caching\DbDependency', + * 'sql' => 'SELECT MAX(lastModified) FROM Post', + * ) + * ~~~ + * + * would make the output cache depends on the last modified time of all posts. + * If any post has its modification time changed, the cached content would be invalidated. + */ + public $dependency; + /** + * @var array list of factors that would cause the variation of the content being cached. + * Each factor is a string representing a variation (e.g. the language, a GET parameter). + * The following variation setting will cause the content to be cached in different versions + * according to the current application language: + * + * ~~~ + * array( + * Yii::$app->language, + * ) + */ + public $variations; + /** + * @var boolean whether to enable the fragment cache. You may use this property to turn on and off + * the fragment cache according to specific setting (e.g. enable fragment cache only for GET requests). + */ + public $enabled = true; + + + public function init() + { + parent::init(); + if ($this->view === null) { + $this->view = Yii::$app->getView(); + } + } + + /** + * 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) + { + $properties = array(); + foreach (array('cache', 'duration', 'dependency', 'variations', 'enabled') as $name) { + $properties[$name] = $this->$name; + } + $id = $this->varyByRoute ? $action->getUniqueId() : __CLASS__; + return $this->view->beginCache($id, $properties); + } + + /** + * This method is invoked right after an action is executed. + * You may override this method to do some postprocessing for the action. + * @param Action $action the action just executed. + */ + public function afterAction($action) + { + $this->view->endCache(); + } +} \ No newline at end of file diff --git a/framework/web/Pagination.php b/framework/web/Pagination.php index 26d670e..1d41c0c 100644 --- a/framework/web/Pagination.php +++ b/framework/web/Pagination.php @@ -1,149 +1,119 @@ <?php /** - * CPagination class file. - * - * @author Qiang Xue <qiang.xue@gmail.com> * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008-2011 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ +namespace yii\web; + +use Yii; + /** - * CPagination represents information relevant to pagination. + * Pagination represents information relevant to pagination of data items. * - * When data needs to be rendered in multiple pages, we can use CPagination to - * represent information such as {@link getItemCount total item count}, - * {@link getPageSize page size}, {@link getCurrentPage current page}, etc. - * These information can be passed to {@link CBasePager pagers} to render - * pagination buttons or links. + * When data needs to be rendered in multiple pages, Pagination can be used to + * represent information such as [[itemCount|total item count]], [[pageSize|page size]], + * [[page|current page]], etc. These information can be passed to [[yii\widgets\Pager|pagers]] + * to render pagination buttons or links. * - * Example: + * The following example shows how to create a pagination object and feed it + * to a pager. * * Controller action: - * <pre> - * function actionIndex(){ - * $criteria=new CDbCriteria(); - * $count=Article::model()->count($criteria); - * $pages=new CPagination($count); * - * // results per page - * $pages->pageSize=10; - * $pages->applyLimit($criteria); - * $models=Article::model()->findAll($criteria); + * ~~~ + * function actionIndex() + * { + * $query = Article::find()->where(array('status' => 1)); + * $countQuery = clone $query; + * $pages = new Pagination($countQuery->count()); + * $models = $query->offset($pages->offset) + * ->limit($pages->limit) + * ->all(); * * $this->render('index', array( - * 'models' => $models, - * 'pages' => $pages + * 'models' => $models, + * 'pages' => $pages, * )); * } - * </pre> + * ~~~ * * View: - * <pre> - * <?php foreach($models as $model): ?> - * // display a model - * <?php endforeach; ?> + * + * ~~~ + * foreach($models as $model) { + * // display $model here + * } * * // display pagination - * <?php $this->widget('CLinkPager', array( + * $this->widget('yii\widgets\LinkPager', array( * 'pages' => $pages, - * )) ?> - * </pre> + * )); + * ~~~ * - * @property integer $pageSize Number of items in each page. Defaults to 10. - * @property integer $itemCount Total number of items. Defaults to 0. * @property integer $pageCount Number of pages. - * @property integer $currentPage The zero-based index of the current page. Defaults to 0. + * @property integer $page The zero-based index of the current page. * @property integer $offset The offset of the data. This may be used to set the * OFFSET value for a SQL statement for fetching the current page of data. * @property integer $limit The limit of the data. This may be used to set the * LIMIT value for a SQL statement for fetching the current page of data. - * This returns the same value as {@link pageSize}. * * @author Qiang Xue <qiang.xue@gmail.com> - * @version $Id$ - * @package system.web - * @since 1.0 + * @since 2.0 */ -class CPagination extends CComponent +class Pagination extends \yii\base\Object { /** - * The default page size. + * @var string name of the parameter storing the current page index. Defaults to 'page'. + * @see params */ - const DEFAULT_PAGE_SIZE = 10; + public $pageVar = 'page'; /** - * @var string name of the GET variable storing the current page index. Defaults to 'page'. + * @var boolean whether to always have the page parameter in the URL created by [[createUrl()]]. + * If false and [[page]] is 0, the page parameter will not be put in the URL. */ - public $pageVar = 'page'; + public $forcePageVar = false; /** - * @var string the route (controller ID and action ID) for displaying the paged contents. - * Defaults to empty string, meaning using the current route. + * @var string the route of the controller action for displaying the paged contents. + * If not set, it means using the currently requested route. */ - public $route = ''; + public $route; /** - * @var array of parameters (name=>value) that should be used instead of GET when generating pagination URLs. - * Defaults to null, meaning using the currently available GET parameters. + * @var array parameters (name=>value) that should be used to obtain the current page number + * and to create new pagination URLs. If not set, $_GET will be used instead. + * + * The array element indexed by [[pageVar]] is considered to be the current page number. + * If the element does not exist, the current page number is considered 0. */ public $params; /** - * @var boolean whether to ensure {@link currentPage} is returning a valid page number. - * When this property is true, the value returned by {@link currentPage} will always be between - * 0 and ({@link pageCount}-1). Because {@link pageCount} relies on the correct value of {@link itemCount}, - * it means you must have knowledge about the total number of data items when you want to access {@link currentPage}. - * This is fine for SQL-based queries, but may not be feasible for other kinds of queries (e.g. MongoDB). - * In those cases, you may set this property to be false to skip the validation (you may need to validate yourself then). - * Defaults to true. - * @since 1.1.4 + * @var boolean whether to check if [[page]] is within valid range. + * When this property is true, the value of [[page]] will always be between 0 and ([[pageCount]]-1). + * Because [[pageCount]] relies on the correct value of [[itemCount]] which may not be available + * in some cases (e.g. MongoDB), you may want to set this property to be false to disable the page + * number validation. By doing so, [[page]] will return the value indexed by [[pageVar]] in [[params]]. */ - public $validateCurrentPage = true; - - private $_pageSize = self::DEFAULT_PAGE_SIZE; - private $_itemCount = 0; - private $_currentPage; - + public $validatePage = true; /** - * Constructor. - * @param integer $itemCount total number of items. + * @var integer number of items on each page. Defaults to 10. + * If it is less than 1, it means the page size is infinite, and thus a single page contains all items. */ - public function __construct($itemCount = 0) - { - $this->setItemCount($itemCount); - } - + public $pageSize = 10; /** - * @return integer number of items in each page. Defaults to 10. + * @var integer total number of items. */ - public function getPageSize() - { - return $this->_pageSize; - } + public $itemCount; /** - * @param integer $value number of items in each page - */ - public function setPageSize($value) - { - if (($this->_pageSize = $value) <= 0) { - $this->_pageSize = self::DEFAULT_PAGE_SIZE; - } - } - - /** - * @return integer total number of items. Defaults to 0. + * Constructor. + * @param integer $itemCount total number of items. + * @param array $config name-value pairs that will be used to initialize the object properties */ - public function getItemCount() + public function __construct($itemCount, $config = array()) { - return $this->_itemCount; - } - - /** - * @param integer $value total number of items. - */ - public function setItemCount($value) - { - if (($this->_itemCount = $value) < 0) { - $this->_itemCount = 0; - } + $this->itemCount = $itemCount; + parent::__construct($config); } /** @@ -151,94 +121,88 @@ class CPagination extends CComponent */ public function getPageCount() { - return (int)(($this->_itemCount + $this->_pageSize - 1) / $this->_pageSize); + if ($this->pageSize < 1) { + return $this->itemCount > 0 ? 1 : 0; + } else { + $itemCount = $this->itemCount < 0 ? 0 : (int)$this->itemCount; + return (int)(($itemCount + $this->pageSize - 1) / $this->pageSize); + } } + private $_page; + /** + * Returns the zero-based current page number. * @param boolean $recalculate whether to recalculate the current page based on the page size and item count. - * @return integer the zero-based index of the current page. Defaults to 0. + * @return integer the zero-based current page number. */ - public function getCurrentPage($recalculate = true) + public function getPage($recalculate = false) { - if ($this->_currentPage === null || $recalculate) { - if (isset($_GET[$this->pageVar])) { - $this->_currentPage = (int)$_GET[$this->pageVar] - 1; - if ($this->validateCurrentPage) { + if ($this->_page === null || $recalculate) { + $params = $this->params === null ? $_GET : $this->params; + if (isset($params[$this->pageVar]) && is_scalar($params[$this->pageVar])) { + $this->_page = (int)$params[$this->pageVar] - 1; + if ($this->validatePage) { $pageCount = $this->getPageCount(); - if ($this->_currentPage >= $pageCount) { - $this->_currentPage = $pageCount - 1; + if ($this->_page >= $pageCount) { + $this->_page = $pageCount - 1; } } - if ($this->_currentPage < 0) { - $this->_currentPage = 0; + if ($this->_page < 0) { + $this->_page = 0; } } else { - $this->_currentPage = 0; + $this->_page = 0; } } - return $this->_currentPage; + return $this->_page; } /** + * Sets the current page number. * @param integer $value the zero-based index of the current page. */ - public function setCurrentPage($value) + public function setPage($value) { - $this->_currentPage = $value; - $_GET[$this->pageVar] = $value + 1; + $this->_page = $value; } /** - * Creates the URL suitable for pagination. - * This method is mainly called by pagers when creating URLs used to - * perform pagination. The default implementation is to call - * the controller's createUrl method with the page information. - * You may override this method if your URL scheme is not the same as - * the one supported by the controller's createUrl method. - * @param CController $controller the controller that will create the actual URL - * @param integer $page the page that the URL should point to. This is a zero-based index. + * Creates the URL suitable for pagination with the specified page number. + * This method is mainly called by pagers when creating URLs used to perform pagination. + * @param integer $page the zero-based page number that the URL should point to. * @return string the created URL + * @see params + * @see forcePageVar */ - public function createPageUrl($controller, $page) + public function createUrl($page) { $params = $this->params === null ? $_GET : $this->params; - if ($page > 0) // page 0 is the default - { + if ($page > 0 || $page >= 0 && $this->forcePageVar) { $params[$this->pageVar] = $page + 1; } else { unset($params[$this->pageVar]); } - return $controller->createUrl($this->route, $params); - } - - /** - * Applies LIMIT and OFFSET to the specified query criteria. - * @param CDbCriteria $criteria the query criteria that should be applied with the limit - */ - public function applyLimit($criteria) - { - $criteria->limit = $this->getLimit(); - $criteria->offset = $this->getOffset(); + $route = $this->route === null ? Yii::$app->controller->route : $this->route; + return Yii::$app->getUrlManager()->createUrl($route, $params); } /** * @return integer the offset of the data. This may be used to set the * OFFSET value for a SQL statement for fetching the current page of data. - * @since 1.1.0 */ public function getOffset() { - return $this->getCurrentPage() * $this->getPageSize(); + return $this->pageSize < 1 ? 0 : $this->getPage() * $this->pageSize; } /** * @return integer the limit of the data. This may be used to set the * LIMIT value for a SQL statement for fetching the current page of data. - * This returns the same value as {@link pageSize}. - * @since 1.1.0 + * Note that if the page size is infinite, a value -1 will be returned. */ public function getLimit() { - return $this->getPageSize(); + return $this->pageSize < 1 ? -1 : $this->pageSize; } } \ No newline at end of file diff --git a/framework/web/Request.php b/framework/web/Request.php index 09e4864..093a394 100644 --- a/framework/web/Request.php +++ b/framework/web/Request.php @@ -1,15 +1,15 @@ <?php /** - * Request class file. - * * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ namespace yii\web; -use \yii\base\InvalidConfigException; +use Yii; +use yii\base\HttpException; +use yii\base\InvalidConfigException; /** * @author Qiang Xue <qiang.xue@gmail.com> @@ -18,19 +18,13 @@ use \yii\base\InvalidConfigException; class Request extends \yii\base\Request { /** - * @var boolean whether cookies should be validated to ensure they are not tampered. Defaults to false. + * @var boolean whether cookies should be validated to ensure they are not tampered. Defaults to true. */ - public $enableCookieValidation = false; + public $enableCookieValidation = true; /** - * @var boolean whether to enable CSRF (Cross-Site Request Forgery) validation. Defaults to false. - * By setting this property to true, forms submitted to an Yii Web application must be originated - * from the same application. If not, a 400 HTTP exception will be raised. - * Note, this feature requires that the user client accepts cookie. - * You also need to use {@link CHtml::form} or {@link CHtml::statefulForm} to generate - * the needed HTML forms in your pages. - * @see http://seclab.stanford.edu/websec/csrf/csrf.pdf + * @var string the secret key used for cookie validation. If not set, a random key will be generated and used. */ - public $enableCsrfValidation = false; + public $cookieValidationKey; /** * @var string|boolean the name of the POST parameter that is used to indicate if a request is a PUT or DELETE * request tunneled through POST. If false, it means disabling REST request tunneled through POST. @@ -38,85 +32,43 @@ class Request extends \yii\base\Request * @see getRequestMethod * @see getRestParams */ - public $restPostVar = '_method'; - /** - * @var string the name of the token used to prevent CSRF. Defaults to 'YII_CSRF_TOKEN'. - * This property is effective only when {@link enableCsrfValidation} is true. - */ - public $csrfTokenName = 'YII_CSRF_TOKEN'; - /** - * @var array the property values (in name-value pairs) used to initialize the CSRF cookie. - * Any property of {@link CHttpCookie} may be initialized. - * This property is effective only when {@link enableCsrfValidation} is true. - */ - public $csrfCookie; + public $restVar = '_method'; private $_cookies; /** - * Initializes the application component. - * This method overrides the parent implementation by preprocessing - * the user request data. + * Resolves the current request into a route and the associated parameters. + * @return array the first element is the route, and the second is the associated parameters. + * @throws HttpException if the request cannot be resolved. */ - public function init() + public function resolve() { - parent::init(); - $this->normalizeRequest(); - } + Yii::setAlias('@www', $this->getBaseUrl()); - /** - * Normalizes the request data. - * This method strips off slashes in request data if get_magic_quotes_gpc() returns true. - * It also performs CSRF validation if {@link enableCsrfValidation} is true. - */ - protected function normalizeRequest() - { - if (get_magic_quotes_gpc()) { - if (isset($_GET)) { - $_GET = $this->stripSlashes($_GET); - } - if (isset($_POST)) { - $_POST = $this->stripSlashes($_POST); - } - if (isset($_REQUEST)) { - $_REQUEST = $this->stripSlashes($_REQUEST); - } - if (isset($_COOKIE)) { - $_COOKIE = $this->stripSlashes($_COOKIE); - } - } - - if ($this->enableCsrfValidation) { - \Yii::$application->on('beginRequest', array($this, 'validateCsrfToken')); + $result = Yii::$app->getUrlManager()->parseRequest($this); + if ($result !== false) { + list ($route, $params) = $result; + $params = array_merge($_GET, $params); + return array($route, $params); + } else { + throw new HttpException(404, Yii::t('yii|Page not found.')); } } /** - * Strips slashes from input data. - * This method is applied when magic quotes is enabled. - * @param mixed $data input data to be processed - * @return mixed processed data - */ - public function stripSlashes($data) - { - return is_array($data) ? array_map(array($this, 'stripSlashes'), $data) : stripslashes($data); - } - - /** * Returns the method of the current request (e.g. GET, POST, HEAD, PUT, DELETE). * @return string request method, such as GET, POST, HEAD, PUT, DELETE. * The value returned is turned into upper case. */ public function getRequestMethod() { - if ($this->restPostVar !== false && isset($_POST[$this->restPostVar])) { - return strtoupper($_POST[$this->restPostVar]); + if ($this->restVar !== false && isset($_POST[$this->restVar])) { + return strtoupper($_POST[$this->restVar]); } else { return isset($_SERVER['REQUEST_METHOD']) ? strtoupper($_SERVER['REQUEST_METHOD']) : 'GET'; } } - /** * Returns whether this is a POST request. * @return boolean whether this is a POST request. @@ -154,7 +106,7 @@ class Request extends \yii\base\Request } /** - * Returns whether this is an Adobe Flash or Adobe Flex request. + * Returns whether this is an Adobe Flash or Flex request. * @return boolean whether this is an Adobe Flash or Adobe Flex request. */ public function getIsFlashRequest() @@ -173,42 +125,41 @@ class Request extends \yii\base\Request public function getRestParams() { if ($this->_restParams === null) { - if ($this->restPostVar !== false && isset($_POST[$this->restPostVar])) { + if ($this->restVar !== false && isset($_POST[$this->restVar])) { $this->_restParams = $_POST; } else { $this->_restParams = array(); if (function_exists('mb_parse_str')) { - mb_parse_str(file_get_contents('php://input'), $this->_restParams); + mb_parse_str($this->getRawBody(), $this->_restParams); } else { - parse_str(file_get_contents('php://input'), $this->_restParams); + parse_str($this->getRawBody(), $this->_restParams); } } } return $this->_restParams; } + private $_rawBody; + /** - * Sets the RESTful parameters. - * @param array $values the RESTful parameters (name-value pairs) + * Returns the raw HTTP request body. + * @return string the request body */ - public function setRestParams($values) + public function getRawBody() { - $this->_restParams = $values; + if ($this->_rawBody === null) { + $this->_rawBody = file_get_contents('php://input'); + } + return $this->_rawBody; } /** - * Returns the named GET or POST parameter value. - * If the GET or POST parameter does not exist, the second parameter to this method will be returned. - * If both GET and POST contains such a named parameter, the GET parameter takes precedence. - * @param string $name the GET parameter name - * @param mixed $defaultValue the default parameter value if the GET parameter does not exist. - * @return mixed the GET parameter value - * @see getQuery - * @see getPost + * Sets the RESTful parameters. + * @param array $values the RESTful parameters (name-value pairs) */ - public function getParam($name, $defaultValue = null) + public function setRestParams($values) { - return isset($_GET[$name]) ? $_GET[$name] : (isset($_POST[$name]) ? $_POST[$name] : $defaultValue); + $this->_restParams = $values; } /** @@ -230,9 +181,8 @@ class Request extends \yii\base\Request * @param mixed $defaultValue the default parameter value if the GET parameter does not exist. * @return mixed the GET parameter value * @see getPost - * @see getParam */ - public function getQuery($name, $defaultValue = null) + public function getParam($name, $defaultValue = null) { return isset($_GET[$name]) ? $_GET[$name] : $defaultValue; } @@ -244,7 +194,6 @@ class Request extends \yii\base\Request * @param mixed $defaultValue the default parameter value if the POST parameter does not exist. * @return mixed the POST parameter value * @see getParam - * @see getQuery */ public function getPost($name, $defaultValue = null) { @@ -273,16 +222,6 @@ class Request extends \yii\base\Request return $this->getIsPutRequest() ? $this->getRestParam($name, $defaultValue) : null; } - /** - * Returns the currently requested URL. - * This is the same as [[requestUri]]. - * @return string part of the request URL after the host info. - */ - public function getUrl() - { - return $this->getRequestUri(); - } - private $_hostInfo; /** @@ -398,7 +337,7 @@ class Request extends \yii\base\Request * A path info refers to the part that is after the entry script and before the question mark (query string). * The starting and ending slashes are both removed. * @return string part of the request URL that is after the entry script and before the question mark. - * Note, the returned path info is decoded. + * Note, the returned path info is already URL-decoded. * @throws InvalidConfigException if the path info cannot be determined due to unexpected server configuration */ public function getPathInfo() @@ -410,6 +349,16 @@ class Request extends \yii\base\Request } /** + * Sets the path info of the current request. + * This method is mainly provided for testing purpose. + * @param string $value the path info of the current request + */ + public function setPathInfo($value) + { + $this->_pathInfo = trim($value, '/'); + } + + /** * Resolves the path info part of the currently requested URL. * A path info refers to the part that is after the entry script and before the question mark (query string). * The starting and ending slashes are both removed. @@ -419,13 +368,28 @@ class Request extends \yii\base\Request */ protected function resolvePathInfo() { - $pathInfo = $this->getRequestUri(); + $pathInfo = $this->getUrl(); if (($pos = strpos($pathInfo, '?')) !== false) { $pathInfo = substr($pathInfo, 0, $pos); } - $pathInfo = $this->decodeUrl($pathInfo); + $pathInfo = urldecode($pathInfo); + + // try to encode in UTF8 if not so + // http://w3.org/International/questions/qa-forms-utf-8.html + if (!preg_match('%^(?: + [\x09\x0A\x0D\x20-\x7E] # ASCII + | [\xC2-\xDF][\x80-\xBF] # non-overlong 2-byte + | \xE0[\xA0-\xBF][\x80-\xBF] # excluding overlongs + | [\xE1-\xEC\xEE\xEF][\x80-\xBF]{2} # straight 3-byte + | \xED[\x80-\x9F][\x80-\xBF] # excluding surrogates + | \xF0[\x90-\xBF][\x80-\xBF]{2} # planes 1-3 + | [\xF1-\xF3][\x80-\xBF]{3} # planes 4-15 + | \xF4[\x80-\x8F][\x80-\xBF]{2} # plane 16 + )*$%xs', $pathInfo)) { + $pathInfo = utf8_encode($pathInfo); + } $scriptUrl = $this->getScriptUrl(); $baseUrl = $this->getBaseUrl(); @@ -436,58 +400,48 @@ class Request extends \yii\base\Request } elseif (strpos($_SERVER['PHP_SELF'], $scriptUrl) === 0) { $pathInfo = substr($_SERVER['PHP_SELF'], strlen($scriptUrl)); } else { - return false; + throw new InvalidConfigException('Unable to determine the path info of the current request.'); } return trim($pathInfo, '/'); } /** - * Decodes the given URL. - * This method is an improved variant of the native urldecode() function. It will properly encode - * UTF-8 characters which may be returned by urldecode(). - * @param string $url encoded URL - * @return string decoded URL + * Returns the currently requested absolute URL. + * This is a shortcut to the concatenation of [[hostInfo]] and [[url]]. + * @return string the currently requested absolute URL. */ - public function decodeUrl($url) + public function getAbsoluteUrl() { - $url = urldecode($url); - - // is it UTF-8? - // http://w3.org/International/questions/qa-forms-utf-8.html - if (preg_match('%^(?: - [\x09\x0A\x0D\x20-\x7E] # ASCII - | [\xC2-\xDF][\x80-\xBF] # non-overlong 2-byte - | \xE0[\xA0-\xBF][\x80-\xBF] # excluding overlongs - | [\xE1-\xEC\xEE\xEF][\x80-\xBF]{2} # straight 3-byte - | \xED[\x80-\x9F][\x80-\xBF] # excluding surrogates - | \xF0[\x90-\xBF][\x80-\xBF]{2} # planes 1-3 - | [\xF1-\xF3][\x80-\xBF]{3} # planes 4-15 - | \xF4[\x80-\x8F][\x80-\xBF]{2} # plane 16 - )*$%xs', $url)) { - return $url; - } else { - return utf8_encode($url); - } + return $this->getHostInfo() . $this->getUrl(); } - private $_requestUri; + private $_url; /** - * Returns the request URI portion for the currently requested URL. - * This refers to the portion that is after the [[hostInfo]] part. It includes the [[queryString]] part if any. - * The implementation of this method referenced Zend_Controller_Request_Http in Zend Framework. - * @return string the request URI portion for the currently requested URL. - * Note that the URI returned is URL-encoded. - * @throws InvalidConfigException if the request URI cannot be determined due to unusual server configuration + * Returns the currently requested relative URL. + * This refers to the portion of the URL that is after the [[hostInfo]] part. + * It includes the [[queryString]] part if any. + * @return string the currently requested relative URL. Note that the URI returned is URL-encoded. + * @throws InvalidConfigException if the URL cannot be determined due to unusual server configuration */ - public function getRequestUri() + public function getUrl() { - if ($this->_requestUri === null) { - $this->_requestUri = $this->resolveRequestUri(); + if ($this->_url === null) { + $this->_url = $this->resolveRequestUri(); } + return $this->_url; + } - return $this->_requestUri; + /** + * Sets the currently requested relative URL. + * The URI must refer to the portion that is after [[hostInfo]]. + * Note that the URI should be URL-encoded. + * @param string $value the request URI to be set + */ + public function setUrl($value) + { + $this->_url = $value; } /** @@ -504,11 +458,7 @@ class Request extends \yii\base\Request $requestUri = $_SERVER['HTTP_X_REWRITE_URL']; } elseif (isset($_SERVER['REQUEST_URI'])) { $requestUri = $_SERVER['REQUEST_URI']; - if (!empty($_SERVER['HTTP_HOST'])) { - if (strpos($requestUri, $_SERVER['HTTP_HOST']) !== false) { - $requestUri = preg_replace('/^\w+:\/\/[^\/]+/', '', $requestUri); - } - } else { + if ($requestUri !== '' && $requestUri[0] !== '/') { $requestUri = preg_replace('/^(http|https):\/\/[^\/]+/i', '', $requestUri); } } elseif (isset($_SERVER['ORIG_PATH_INFO'])) { // IIS 5.0 CGI @@ -580,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'; } @@ -594,49 +544,6 @@ class Request extends \yii\base\Request return isset($_SERVER['REMOTE_HOST']) ? $_SERVER['REMOTE_HOST'] : null; } - private $_scriptFile; - - /** - * Returns entry script file path. - * @return string entry script file path (processed w/ realpath()) - * @throws InvalidConfigException if the entry script file path cannot be determined automatically. - */ - public function getScriptFile() - { - if ($this->_scriptFile === null) { - $this->setScriptFile($_SERVER['SCRIPT_FILENAME']); - } - return $this->_scriptFile; - } - - /** - * Sets the entry script file path. - * The entry script file path can normally be determined based on the `SCRIPT_FILENAME` SERVER variable. - * However, in some server configuration, this may not be correct or feasible. - * This setter is provided so that the entry script file path can be manually specified. - * @param string $value the entry script file path - * @throws InvalidConfigException if the provided entry script file path is invalid. - */ - public function setScriptFile($value) - { - $this->_scriptFile = realpath($value); - if ($this->_scriptFile === false || !is_file($this->_scriptFile)) { - throw new InvalidConfigException('Unable to determine the entry script file path.'); - } - } - - /** - * Returns information about the capabilities of user browser. - * @param string $userAgent the user agent to be analyzed. Defaults to null, meaning using the - * current User-Agent HTTP header information. - * @return array user browser capabilities. - * @see http://www.php.net/manual/en/function.get-browser.php - */ - public function getBrowser($userAgent = null) - { - return get_browser($userAgent, true); - } - /** * Returns user browser accept types, null if not present. * @return string user browser accept types, null if not present @@ -744,88 +651,31 @@ class Request extends \yii\base\Request return isset($languages[0]) ? $languages[0] : false; } - /** * Returns the cookie collection. - * The result can be used like an associative array. Adding {@link CHttpCookie} objects - * to the collection will send the cookies to the client; and removing the objects - * from the collection will delete those cookies on the client. - * @return CCookieCollection the cookie collection. + * Through the returned cookie collection, you may access a cookie using the following syntax: + * + * ~~~ + * $cookie = $request->cookies['name'] + * if ($cookie !== null) { + * $value = $cookie->value; + * } + * + * // alternatively + * $value = $request->cookies->getValue('name'); + * ~~~ + * + * @return CookieCollection the cookie collection. */ public function getCookies() { - if ($this->_cookies !== null) { - return $this->_cookies; - } else { - return $this->_cookies = new CCookieCollection($this); - } - } - - private $_csrfToken; - - /** - * Returns the random token used to perform CSRF validation. - * The token will be read from cookie first. If not found, a new token - * will be generated. - * @return string the random token for CSRF validation. - * @see enableCsrfValidation - */ - public function getCsrfToken() - { - if ($this->_csrfToken === null) { - $cookie = $this->getCookies()->itemAt($this->csrfTokenName); - if (!$cookie || ($this->_csrfToken = $cookie->value) == null) { - $cookie = $this->createCsrfCookie(); - $this->_csrfToken = $cookie->value; - $this->getCookies()->add($cookie->name, $cookie); - } - } - - return $this->_csrfToken; - } - - /** - * Creates a cookie with a randomly generated CSRF token. - * Initial values specified in {@link csrfCookie} will be applied - * to the generated cookie. - * @return CHttpCookie the generated cookie - * @see enableCsrfValidation - */ - protected function createCsrfCookie() - { - $cookie = new CHttpCookie($this->csrfTokenName, sha1(uniqid(mt_rand(), true))); - if (is_array($this->csrfCookie)) { - foreach ($this->csrfCookie as $name => $value) { - $cookie->$name = $value; - } - } - return $cookie; - } - - /** - * Performs the CSRF validation. - * This is the event handler responding to {@link CApplication::onBeginRequest}. - * The default implementation will compare the CSRF token obtained - * from a cookie and from a POST field. If they are different, a CSRF attack is detected. - * @param CEvent $event event parameter - * @throws CHttpException if the validation fails - */ - public function validateCsrfToken($event) - { - if ($this->getIsPostRequest()) { - // only validate POST requests - $cookies = $this->getCookies(); - if ($cookies->contains($this->csrfTokenName) && isset($_POST[$this->csrfTokenName])) { - $tokenFromCookie = $cookies->itemAt($this->csrfTokenName)->value; - $tokenFromPost = $_POST[$this->csrfTokenName]; - $valid = $tokenFromCookie === $tokenFromPost; - } else { - $valid = false; - } - if (!$valid) { - throw new CHttpException(400, Yii::t('yii', 'The CSRF token could not be verified.')); - } + if ($this->_cookies === null) { + $this->_cookies = new CookieCollection(array( + 'enableValidation' => $this->enableCookieValidation, + 'validationKey' => $this->cookieValidationKey, + )); } + return $this->_cookies; } } diff --git a/framework/web/Response.php b/framework/web/Response.php index 73a28e3..d23c5b9 100644 --- a/framework/web/Response.php +++ b/framework/web/Response.php @@ -1,15 +1,14 @@ <?php /** - * Response class file. - * * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ namespace yii\web; -use yii\util\FileHelper; +use Yii; +use yii\helpers\FileHelper; /** * @author Qiang Xue <qiang.xue@gmail.com> @@ -82,6 +81,11 @@ class Response extends \yii\base\Response * If this option is disabled by the web server, when this method is called a download configuration dialog * will open but the downloaded file will have 0 bytes. * + * <b>Known issues</b>: + * There is a Bug with Internet Explorer 6, 7 and 8 when X-SENDFILE is used over an SSL connection, it will show + * an error message like this: "Internet Explorer was not able to open this Internet site. The requested site is either unavailable or cannot be found.". + * You can work around this problem by removing the <code>Pragma</code>-header. + * * <b>Example</b>: * <pre> * <?php @@ -102,63 +106,79 @@ class Response extends \yii\base\Response * <li>forceDownload: specifies whether the file will be downloaded or shown inline, defaults to true. (Since version 1.1.9.)</li> * <li>addHeaders: an array of additional http headers in header-value pairs (available since version 1.1.10)</li> * </ul> - * @todo */ - public function xSendFile($filePath, $options = array()) + public function xSendFile($filePath, $options=array()) { - if (!isset($options['forceDownload']) || $options['forceDownload']) { - $disposition = 'attachment'; - } else { - $disposition = 'inline'; - } + if(!isset($options['forceDownload']) || $options['forceDownload']) + $disposition='attachment'; + else + $disposition='inline'; - if (!isset($options['saveName'])) { - $options['saveName'] = basename($filePath); - } + if(!isset($options['saveName'])) + $options['saveName']=basename($filePath); - if (!isset($options['mimeType'])) { - if (($options['mimeType'] = CFileHelper::getMimeTypeByExtension($filePath)) === null) { - $options['mimeType'] = 'text/plain'; - } + if(!isset($options['mimeType'])) + { + if(($options['mimeType']=CFileHelper::getMimeTypeByExtension($filePath))===null) + $options['mimeType']='text/plain'; } - if (!isset($options['xHeader'])) { - $options['xHeader'] = 'X-Sendfile'; - } + if(!isset($options['xHeader'])) + $options['xHeader']='X-Sendfile'; - if ($options['mimeType'] !== null) { - header('Content-type: ' . $options['mimeType']); + if($options['mimeType'] !== null) + header('Content-type: '.$options['mimeType']); + header('Content-Disposition: '.$disposition.'; filename="'.$options['saveName'].'"'); + if(isset($options['addHeaders'])) + { + foreach($options['addHeaders'] as $header=>$value) + header($header.': '.$value); } - header('Content-Disposition: ' . $disposition . '; filename="' . $options['saveName'] . '"'); - if (isset($options['addHeaders'])) { - foreach ($options['addHeaders'] as $header => $value) { - header($header . ': ' . $value); - } - } - header(trim($options['xHeader']) . ': ' . $filePath); + header(trim($options['xHeader']).': '.$filePath); - if (!isset($options['terminate']) || $options['terminate']) { + if(!isset($options['terminate']) || $options['terminate']) Yii::app()->end(); - } } - /** * Redirects the browser to the specified URL. - * @param string $url URL to be redirected to. If the URL is a relative one, the base URL of - * the application will be inserted at the beginning. + * @param string $url URL to be redirected to. Note that when URL is not + * absolute (not starting with "/") it will be relative to current request URL. * @param boolean $terminate whether to terminate the current application * @param integer $statusCode the HTTP status code. Defaults to 302. See {@link http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html} * for details about HTTP status code. */ - public function redirect($url, $terminate = true, $statusCode = 302) + public function redirect($url,$terminate=true,$statusCode=302) { - if (strpos($url, '/') === 0) { - $url = $this->getHostInfo() . $url; - } - header('Location: ' . $url, true, $statusCode); - if ($terminate) { + if(strpos($url,'/')===0 && strpos($url,'//')!==0) + $url=$this->getHostInfo().$url; + header('Location: '.$url, true, $statusCode); + if($terminate) Yii::app()->end(); - } + } + + + /** + * Returns the cookie collection. + * Through the returned cookie collection, you add or remove cookies as follows, + * + * ~~~ + * // add a cookie + * $response->cookies->add(new Cookie(array( + * 'name' => $name, + * 'value' => $value, + * )); + * + * // remove a cookie + * $response->cookies->remove('name'); + * // alternatively + * unset($response->cookies['name']); + * ~~~ + * + * @return CookieCollection the cookie collection. + */ + public function getCookies() + { + return Yii::$app->getRequest()->getCookies(); } } diff --git a/framework/web/Session.php b/framework/web/Session.php index 4544fc0..c289db2 100644 --- a/framework/web/Session.php +++ b/framework/web/Session.php @@ -1,82 +1,63 @@ <?php /** - * CHttpSession class file. - * - * @author Qiang Xue <qiang.xue@gmail.com> * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008-2011 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ +namespace yii\web; + +use Yii; +use yii\base\Component; +use yii\base\InvalidParamException; + /** - * CHttpSession provides session-level data management and the related configurations. - * - * To start the session, call {@link open()}; To complete and send out session data, call {@link close()}; - * To destroy the session, call {@link destroy()}. + * Session provides session data management and the related configurations. * - * If {@link autoStart} is set true, the session will be started automatically - * when the application component is initialized by the application. + * 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()]]. * - * CHttpSession can be used like an array to set and get session data. For example, - * <pre> - * $session=new CHttpSession; - * $session->open(); - * $value1=$session['name1']; // get session variable 'name1' - * $value2=$session['name2']; // get session variable 'name2' - * foreach($session as $name=>$value) // traverse all session variables - * $session['name3']=$value3; // set session variable 'name3' - * </pre> + * By default, [[autoStart]] is true which means the session will be started automatically + * when the session component is accessed the first time. * - * The following configurations are available for session: - * <ul> - * <li>{@link setSessionID sessionID};</li> - * <li>{@link setSessionName sessionName};</li> - * <li>{@link autoStart};</li> - * <li>{@link setSavePath savePath};</li> - * <li>{@link setCookieParams cookieParams};</li> - * <li>{@link setGCProbability gcProbability};</li> - * <li>{@link setCookieMode cookieMode};</li> - * <li>{@link setUseTransparentSessionID useTransparentSessionID};</li> - * <li>{@link setTimeout timeout}.</li> - * </ul> - * See the corresponding setter and getter documentation for more information. - * Note, these properties must be set before the session is started. + * Session can be used like an array to set and get session data. For example, * - * CHttpSession can be extended to support customized session storage. - * Override {@link openSession}, {@link closeSession}, {@link readSession}, - * {@link writeSession}, {@link destroySession} and {@link gcSession} - * and set {@link useCustomStorage} to true. - * Then, the session data will be stored and retrieved using the above methods. + * ~~~ + * $session = new Session; + * $session->open(); + * $value1 = $session['name1']; // get session variable 'name1' + * $value2 = $session['name2']; // get session variable 'name2' + * foreach ($session as $name => $value) // traverse all session variables + * $session['name3'] = $value3; // set session variable 'name3' + * ~~~ * - * CHttpSession is a Web application component that can be accessed via - * {@link CWebApplication::getSession()}. + * Session can be extended to support customized session storage. + * To do so, override [[useCustomStorage()]] so that it returns true, and + * override these methods with the actual logic about using custom storage: + * [[openSession()]], [[closeSession()]], [[readSession()]], [[writeSession()]], + * [[destroySession()]] and [[gcSession()]]. * - * @property boolean $useCustomStorage Whether to use custom storage. - * @property boolean $isStarted Whether the session has started. - * @property string $sessionID The current session ID. - * @property string $sessionName 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 integer $gCProbability The probability (percentage) that the gc (garbage collection) process is started on every session initialization, defaults to 1 meaning 1% chance. - * @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 CHttpSessionIterator $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 <qiang.xue@gmail.com> - * @version $Id$ - * @package system.web - * @since 1.0 + * @since 2.0 */ -class CHttpSession extends CApplicationComponent implements IteratorAggregate,ArrayAccess,Countable +class Session extends Component implements \IteratorAggregate, \ArrayAccess, \Countable { /** - * @var boolean whether the session should be automatically started when the session application component is initialized, defaults to true. + * @var boolean whether the session should be automatically started when the session component is initialized. */ - public $autoStart=true; - + public $autoStart = true; + /** + * @var string the name of the session variable that stores the flash message data. + */ + public $flashVar = '__flash'; /** * Initializes the application component. @@ -85,18 +66,17 @@ class CHttpSession extends CApplicationComponent implements IteratorAggregate,Ar public function init() { parent::init(); - if($this->autoStart) + if ($this->autoStart) { $this->open(); - register_shutdown_function(array($this,'close')); + } + register_shutdown_function(array($this, 'close')); } /** * Returns a value indicating whether to use custom session storage. - * This method should be overriden to return true if custom session storage handler should be used. - * If returning true, make sure the methods {@link openSession}, {@link closeSession}, {@link readSession}, - * {@link writeSession}, {@link destroySession}, and {@link gcSession} are overridden in child - * class, because they will be used as the callback handlers. - * The default implementation always return false. + * This method should be overridden to return true by child classes that implement custom session storage. + * To implement custom session storage, override these methods: [[openSession()]], [[closeSession()]], + * [[readSession()]], [[writeSession()]], [[destroySession()]] and [[gcSession()]]. * @return boolean whether to use custom storage. */ public function getUseCustomStorage() @@ -104,25 +84,44 @@ class CHttpSession extends CApplicationComponent implements IteratorAggregate,Ar return false; } + private $_opened = false; + /** - * Starts the session if it has not started yet. + * Starts the session. */ public function open() { - if($this->getUseCustomStorage()) - @session_set_save_handler(array($this,'openSession'),array($this,'closeSession'),array($this,'readSession'),array($this,'writeSession'),array($this,'destroySession'),array($this,'gcSession')); - - @session_start(); - if(YII_DEBUG && session_id()=='') - { - $message=Yii::t('yii','Failed to start session.'); - if(function_exists('error_get_last')) - { - $error=error_get_last(); - if(isset($error['message'])) - $message=$error['message']; + // this is available in PHP 5.4.0+ + if (function_exists('session_status')) { + if (session_status() == PHP_SESSION_ACTIVE) { + $this->_opened = true; + return; + } + } + + if (!$this->_opened) { + if ($this->getUseCustomStorage()) { + @session_set_save_handler( + array($this, 'openSession'), + array($this, 'closeSession'), + array($this, 'readSession'), + array($this, 'writeSession'), + array($this, 'destroySession'), + array($this, 'gcSession') + ); + } + + @session_start(); + + if (session_id() == '') { + $this->_opened = false; + $error = error_get_last(); + $message = isset($error['message']) ? $error['message'] : 'Failed to start session.'; + Yii::error($message, __CLASS__); + } else { + $this->_opened = true; + $this->updateFlashCounters(); } - Yii::log($message, CLogger::LEVEL_WARNING, 'system.web.CHttpSession'); } } @@ -131,8 +130,10 @@ class CHttpSession extends CApplicationComponent implements IteratorAggregate,Ar */ public function close() { - if(session_id()!=='') + $this->_opened = false; + if (session_id() !== '') { @session_write_close(); + } } /** @@ -140,8 +141,7 @@ class CHttpSession extends CApplicationComponent implements IteratorAggregate,Ar */ public function destroy() { - if(session_id()!=='') - { + if (session_id() !== '') { @session_unset(); @session_destroy(); } @@ -150,15 +150,21 @@ class CHttpSession extends CApplicationComponent implements IteratorAggregate,Ar /** * @return boolean whether the session has started */ - public function getIsStarted() + public function getIsActive() { - return session_id()!==''; + if (function_exists('session_status')) { + // available in PHP 5.4.0+ + return session_status() == PHP_SESSION_ACTIVE; + } else { + // this is not very reliable + return $this->_opened && session_id() !== ''; + } } /** * @return string the current session ID */ - public function getSessionID() + public function getId() { return session_id(); } @@ -166,18 +172,17 @@ class CHttpSession extends CApplicationComponent implements IteratorAggregate,Ar /** * @param string $value the session ID for the current session */ - public function setSessionID($value) + public function setId($value) { session_id($value); } /** - * Updates the current session id with a newly generated one . - * Please refer to {@link http://php.net/session_regenerate_id} for more details. + * Updates the current session ID with a newly generated one . + * Please refer to [[http://php.net/session_regenerate_id]] for more details. * @param boolean $deleteOldSession Whether to delete the old associated session file or not. - * @since 1.1.8 */ - public function regenerateID($deleteOldSession=false) + public function regenerateID($deleteOldSession = false) { session_regenerate_id($deleteOldSession); } @@ -185,15 +190,16 @@ class CHttpSession extends CApplicationComponent implements IteratorAggregate,Ar /** * @return string the current session name */ - public function getSessionName() + public function getName() { return session_name(); } /** - * @param string $value the session name for the current session, must be an alphanumeric string, defaults to PHPSESSID + * @param string $value the session name for the current session, must be an alphanumeric string. + * It defaults to "PHPSESSID". */ - public function setSessionName($value) + public function setName($value) { session_name($value); } @@ -207,16 +213,17 @@ class CHttpSession extends CApplicationComponent implements IteratorAggregate,Ar } /** - * @param string $value the current session save path - * @throws CException if the path is not a valid directory + * @param string $value the current session save path. This can be either a directory name or a path alias. + * @throws InvalidParamException if the path is not a valid directory */ public function setSavePath($value) { - if(is_dir($value)) - session_save_path($value); - else - throw new CException(Yii::t('yii','CHttpSession.savePath "{path}" is not a valid directory.', - array('{path}'=>$value))); + $path = Yii::getAlias($value); + if (is_dir($path)) { + session_save_path($path); + } else { + throw new InvalidParamException("Session save path is not a valid directory: $value"); + } } /** @@ -232,80 +239,83 @@ class CHttpSession extends CApplicationComponent implements IteratorAggregate,Ar * Sets the session cookie parameters. * The effect of this method only lasts for the duration of the script. * Call this method before the session starts. - * @param array $value cookie parameters, valid keys include: lifetime, path, domain, secure. + * @param array $value cookie parameters, valid keys include: lifetime, path, domain, secure and httponly. + * @throws InvalidParamException if the parameters are incomplete. * @see http://us2.php.net/manual/en/function.session-set-cookie-params.php */ public function setCookieParams($value) { - $data=session_get_cookie_params(); + $data = session_get_cookie_params(); extract($data); extract($value); - if(isset($httponly)) - session_set_cookie_params($lifetime,$path,$domain,$secure,$httponly); - else - session_set_cookie_params($lifetime,$path,$domain,$secure); + if (isset($lifetime, $path, $domain, $secure, $httponly)) { + session_set_cookie_params($lifetime, $path, $domain, $secure, $httponly); + } else { + throw new InvalidParamException('Please make sure these parameters are provided: lifetime, path, domain, secure and httponly.'); + } } /** - * @return string how to use cookie to store session ID. Defaults to 'Allow'. + * Returns the value indicating whether cookies should be used to store session IDs. + * @return boolean|null the value indicating whether cookies should be used to store session IDs. + * @see setUseCookies() */ - public function getCookieMode() + public function getUseCookies() { - if(ini_get('session.use_cookies')==='0') - return 'none'; - else if(ini_get('session.use_only_cookies')==='0') - return 'allow'; - else - return 'only'; + if (ini_get('session.use_cookies') === '0') { + return false; + } elseif (ini_get('session.use_only_cookies') === '1') { + return true; + } else { + return null; + } } /** - * @param string $value how to use cookie to store session ID. Valid values include 'none', 'allow' and 'only'. + * Sets the value indicating whether cookies should be used to store session IDs. + * Three states are possible: + * + * - true: cookies and only cookies will be used to store session IDs. + * - false: cookies will not be used to store session IDs. + * - null: if possible, cookies will be used to store session IDs; if not, other mechanisms will be used (e.g. GET parameter) + * + * @param boolean|null $value the value indicating whether cookies should be used to store session IDs. */ - public function setCookieMode($value) + public function setUseCookies($value) { - if($value==='none') - { - ini_set('session.use_cookies','0'); - ini_set('session.use_only_cookies','0'); - } - else if($value==='allow') - { - ini_set('session.use_cookies','1'); - ini_set('session.use_only_cookies','0'); + if ($value === false) { + ini_set('session.use_cookies', '0'); + ini_set('session.use_only_cookies', '0'); + } elseif ($value === true) { + ini_set('session.use_cookies', '1'); + ini_set('session.use_only_cookies', '1'); + } else { + ini_set('session.use_cookies', '1'); + ini_set('session.use_only_cookies', '0'); } - else if($value==='only') - { - ini_set('session.use_cookies','1'); - ini_set('session.use_only_cookies','1'); - } - else - throw new CException(Yii::t('yii','CHttpSession.cookieMode can only be "none", "allow" or "only".')); } /** - * @return integer the probability (percentage) that the gc (garbage collection) process is started on every session initialization, defaults to 1 meaning 1% chance. + * @return float the probability (percentage) that the GC (garbage collection) process is started on every session initialization, defaults to 1 meaning 1% chance. */ public function getGCProbability() { - return (int)ini_get('session.gc_probability'); + return (float)(ini_get('session.gc_probability') / ini_get('session.gc_divisor') * 100); } /** - * @param integer $value the probability (percentage) that the gc (garbage collection) process is started on every session initialization. - * @throws CException if the value is beyond [0,100] + * @param float $value the probability (percentage) that the GC (garbage collection) process is started on every session initialization. + * @throws InvalidParamException if the value is not between 0 and 100. */ public function setGCProbability($value) { - $value=(int)$value; - if($value>=0 && $value<=100) - { - ini_set('session.gc_probability',$value); - ini_set('session.gc_divisor','100'); + if ($value >= 0 && $value <= 100) { + // percent * 21474837 / 2147483647 ≈ percent * 0.01 + ini_set('session.gc_probability', floor($value * 21474836.47)); + ini_set('session.gc_divisor', 2147483647); + } else { + throw new InvalidParamException('GCProbability must be a value between 0 and 100.'); } - else - throw new CException(Yii::t('yii','CHttpSession.gcProbability "{value}" is invalid. It must be an integer between 0 and 100.', - array('{value}'=>$value))); } /** @@ -313,7 +323,7 @@ class CHttpSession extends CApplicationComponent implements IteratorAggregate,Ar */ public function getUseTransparentSessionID() { - return ini_get('session.use_trans_sid')==1; + return ini_get('session.use_trans_sid') == 1; } /** @@ -321,11 +331,12 @@ class CHttpSession extends CApplicationComponent implements IteratorAggregate,Ar */ public function setUseTransparentSessionID($value) { - ini_set('session.use_trans_sid',$value?'1':'0'); + ini_set('session.use_trans_sid', $value ? '1' : '0'); } /** - * @return integer the number of seconds after which data will be seen as 'garbage' and cleaned up, defaults to 1440 seconds. + * @return integer the number of seconds after which data will be seen as 'garbage' and cleaned up. + * The default value is 1440 seconds (or the value of "session.gc_maxlifetime" set in php.ini). */ public function getTimeout() { @@ -337,25 +348,25 @@ class CHttpSession extends CApplicationComponent implements IteratorAggregate,Ar */ public function setTimeout($value) { - ini_set('session.gc_maxlifetime',$value); + ini_set('session.gc_maxlifetime', $value); } /** * Session open handler. - * This method should be overridden if {@link useCustomStorage} is set true. + * This method should be overridden if [[useCustomStorage()]] returns true. * Do not call this method directly. * @param string $savePath session save path * @param string $sessionName session name * @return boolean whether session is opened successfully */ - public function openSession($savePath,$sessionName) + public function openSession($savePath, $sessionName) { return true; } /** * Session close handler. - * This method should be overridden if {@link useCustomStorage} is set true. + * This method should be overridden if [[useCustomStorage()]] returns true. * Do not call this method directly. * @return boolean whether session is closed successfully */ @@ -366,7 +377,7 @@ class CHttpSession extends CApplicationComponent implements IteratorAggregate,Ar /** * Session read handler. - * This method should be overridden if {@link useCustomStorage} is set true. + * This method should be overridden if [[useCustomStorage()]] returns true. * Do not call this method directly. * @param string $id session ID * @return string the session data @@ -378,20 +389,20 @@ class CHttpSession extends CApplicationComponent implements IteratorAggregate,Ar /** * Session write handler. - * This method should be overridden if {@link useCustomStorage} is set true. + * This method should be overridden if [[useCustomStorage()]] returns true. * Do not call this method directly. * @param string $id session ID * @param string $data session data * @return boolean whether session write is successful */ - public function writeSession($id,$data) + public function writeSession($id, $data) { return true; } /** * Session destroy handler. - * This method should be overridden if {@link useCustomStorage} is set true. + * This method should be overridden if [[useCustomStorage()]] returns true. * Do not call this method directly. * @param string $id session ID * @return boolean whether session is destroyed successfully @@ -403,7 +414,7 @@ class CHttpSession extends CApplicationComponent implements IteratorAggregate,Ar /** * Session GC (garbage collection) handler. - * This method should be overridden if {@link useCustomStorage} is set true. + * This method should be overridden if [[useCustomStorage()]] returns true. * Do not call this method directly. * @param integer $maxLifetime the number of seconds after which data will be seen as 'garbage' and cleaned up. * @return boolean whether session is GCed successfully @@ -413,16 +424,14 @@ class CHttpSession extends CApplicationComponent implements IteratorAggregate,Ar return true; } - //------ The following methods enable CHttpSession to be CMap-like ----- - /** * Returns an iterator for traversing the session variables. * This method is required by the interface IteratorAggregate. - * @return CHttpSessionIterator an iterator for traversing the session variables. + * @return SessionIterator an iterator for traversing the session variables. */ public function getIterator() { - return new CHttpSessionIterator; + return new SessionIterator; } /** @@ -445,80 +454,59 @@ class CHttpSession extends CApplicationComponent implements IteratorAggregate,Ar } /** - * @return array the list of session variable names - */ - public function getKeys() - { - return array_keys($_SESSION); - } - - /** * Returns the session variable value with the session variable name. - * This method is very similar to {@link itemAt} and {@link offsetGet}, - * except that it will return $defaultValue if the session variable does not exist. - * @param mixed $key the session variable name + * If the session variable does not exist, the `$defaultValue` will be returned. + * @param string $key the session variable name * @param mixed $defaultValue the default value to be returned when the session variable does not exist. * @return mixed the session variable value, or $defaultValue if the session variable does not exist. - * @since 1.1.2 */ - public function get($key,$defaultValue=null) + public function get($key, $defaultValue = null) { return isset($_SESSION[$key]) ? $_SESSION[$key] : $defaultValue; } /** - * Returns the session variable value with the session variable name. - * This method is exactly the same as {@link offsetGet}. - * @param mixed $key the session variable name - * @return mixed the session variable value, null if no such variable exists - */ - public function itemAt($key) - { - return isset($_SESSION[$key]) ? $_SESSION[$key] : null; - } - - /** * 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; + $_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) { - if(isset($_SESSION[$key])) - { - $value=$_SESSION[$key]; + if (isset($_SESSION[$key])) { + $value = $_SESSION[$key]; unset($_SESSION[$key]); return $value; - } - else + } else { return null; + } } /** * Removes all session variables */ - public function clear() + public function removeAll() { - foreach(array_keys($_SESSION) as $key) + foreach (array_keys($_SESSION) as $key) { unset($_SESSION[$key]); + } } /** * @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]); } @@ -532,6 +520,115 @@ class CHttpSession extends CApplicationComponent implements IteratorAggregate,Ar } /** + * 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]); + } + + /** + * Returns a value indicating whether there is a flash message associated with the specified key. + * @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 @@ -556,9 +653,9 @@ class CHttpSession extends CApplicationComponent implements IteratorAggregate,Ar * @param integer $offset the offset to set element * @param mixed $item the element value */ - public function offsetSet($offset,$item) + public function offsetSet($offset, $item) { - $_SESSION[$offset]=$item; + $_SESSION[$offset] = $item; } /** diff --git a/framework/web/SessionIterator.php b/framework/web/SessionIterator.php new file mode 100644 index 0000000..c960dd4 --- /dev/null +++ b/framework/web/SessionIterator.php @@ -0,0 +1,84 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\web; + +/** + * SessionIterator implements an iterator for traversing session variables managed by [[Session]]. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class SessionIterator implements \Iterator +{ + /** + * @var array list of keys in the map + */ + private $_keys; + /** + * @var mixed current key + */ + private $_key; + + /** + * Constructor. + */ + public function __construct() + { + $this->_keys = array_keys($_SESSION); + } + + /** + * Rewinds internal array pointer. + * This method is required by the interface Iterator. + */ + public function rewind() + { + $this->_key = reset($this->_keys); + } + + /** + * Returns the key of the current array element. + * This method is required by the interface Iterator. + * @return mixed the key of the current array element + */ + public function key() + { + return $this->_key; + } + + /** + * Returns the current array element. + * This method is required by the interface Iterator. + * @return mixed the current array element + */ + public function current() + { + return isset($_SESSION[$this->_key]) ? $_SESSION[$this->_key] : null; + } + + /** + * Moves the internal pointer to the next array element. + * This method is required by the interface Iterator. + */ + public function next() + { + do { + $this->_key = next($this->_keys); + } while (!isset($_SESSION[$this->_key]) && $this->_key !== false); + } + + /** + * Returns whether there is an element at current position. + * This method is required by the interface Iterator. + * @return boolean + */ + public function valid() + { + return $this->_key !== false; + } +} diff --git a/framework/web/Sort.php b/framework/web/Sort.php index 12e16a5..7cfeeca 100644 --- a/framework/web/Sort.php +++ b/framework/web/Sort.php @@ -1,470 +1,336 @@ <?php /** - * CSort class file. - * - * @author Qiang Xue <qiang.xue@gmail.com> * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008-2011 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ +namespace yii\web; + +use Yii; +use yii\helpers\Html; + /** - * CSort represents information relevant to sorting. + * Sort represents information relevant to sorting. * * When data needs to be sorted according to one or several attributes, - * we can use CSort to represent the sorting information and generate + * we can use Sort to represent the sorting information and generate * appropriate hyperlinks that can lead to sort actions. * - * CSort is designed to be used together with {@link CActiveRecord}. - * When creating a CSort instance, you need to specify {@link modelClass}. - * You can use CSort to generate hyperlinks by calling {@link link}. - * You can also use CSort to modify a {@link CDbCriteria} instance by calling {@link applyOrder} so that - * it can cause the query results to be sorted according to the specified - * attributes. + * A typical usage example is as follows, + * + * ~~~ + * function actionIndex() + * { + * $sort = new Sort(array( + * 'attributes' => array( + * 'age', + * 'name' => array( + * 'asc' => array('last_name', 'first_name'), + * 'desc' => array('last_name' => true, 'first_name' => true), + * ), + * ), + * )); + * + * $models = Article::find() + * ->where(array('status' => 1)) + * ->orderBy($sort->orders) + * ->all(); * - * In order to prevent SQL injection attacks, CSort ensures that only valid model attributes - * can be sorted. This is determined based on {@link modelClass} and {@link attributes}. - * When {@link attributes} is not set, all attributes belonging to {@link modelClass} - * can be sorted. When {@link attributes} is set, only those attributes declared in the property - * can be sorted. + * $this->render('index', array( + * 'models' => $models, + * 'sort' => $sort, + * )); + * } + * ~~~ * - * By configuring {@link attributes}, one can perform more complex sorts that may - * consist of things like compound attributes (e.g. sort based on the combination of - * first name and last name of users). + * View: * - * The property {@link attributes} should be an array of key-value pairs, where the keys - * represent the attribute names, while the values represent the virtual attribute definitions. - * For more details, please check the documentation about {@link attributes}. + * ~~~ + * // display links leading to sort actions + * echo $sort->link('name', 'Name') . ' | ' . $sort->link('age', 'Age'); * - * @property string $orderBy The order-by columns represented by this sort object. - * This can be put in the ORDER BY clause of a SQL statement. - * @property array $directions Sort directions indexed by attribute names. - * The sort direction. Can be either CSort::SORT_ASC for ascending order or - * CSort::SORT_DESC for descending order. + * foreach($models as $model) { + * // display $model here + * } + * ~~~ + * + * In the above, we declare two [[attributes]] that support sorting: name and age. + * We pass the sort information to the Article query so that the query results are + * sorted by the orders specified by the Sort object. In the view, we show two hyperlinks + * that can lead to pages with the data sorted by the corresponding attributes. + * + * @property array $orders Sort directions indexed by column names. The sort direction + * can be either [[Sort::ASC]] for ascending order or [[Sort::DESC]] for descending order. + * @property array $attributeOrders Sort directions indexed by attribute names. The sort + * direction can be either [[Sort::ASC]] for ascending order or [[Sort::DESC]] for descending order. * * @author Qiang Xue <qiang.xue@gmail.com> - * @version $Id$ - * @package system.web + * @since 2.0 */ -class CSort extends CComponent +class Sort extends \yii\base\Object { /** * Sort ascending - * @since 1.1.10 */ - const SORT_ASC = false; + const ASC = false; /** * Sort descending - * @since 1.1.10 */ - const SORT_DESC = true; + const DESC = true; /** * @var boolean whether the sorting can be applied to multiple attributes simultaneously. * Defaults to false, which means each time the data can only be sorted by one attribute. */ - public $multiSort = false; - /** - * @var string the name of the model class whose attributes can be sorted. - * The model class must be a child class of {@link CActiveRecord}. - */ - public $modelClass; + public $enableMultiSort = false; + /** - * @var array list of attributes that are allowed to be sorted. - * For example, array('user_id','create_time') would specify that only 'user_id' - * and 'create_time' of the model {@link modelClass} can be sorted. - * By default, this property is an empty array, which means all attributes in - * {@link modelClass} are allowed to be sorted. - * - * This property can also be used to specify complex sorting. To do so, - * a virtual attribute can be declared in terms of a key-value pair in the array. - * The key refers to the name of the virtual attribute that may appear in the sort request, - * while the value specifies the definition of the virtual attribute. + * @var array list of attributes that are allowed to be sorted. Its syntax can be + * described using the following example: * - * In the simple case, a key-value pair can be like <code>'user'=>'user_id'</code> - * where 'user' is the name of the virtual attribute while 'user_id' means the virtual - * attribute is the 'user_id' attribute in the {@link modelClass}. - * - * A more flexible way is to specify the key-value pair as - * <pre> - * 'user'=>array( - * 'asc'=>'first_name, last_name', - * 'desc'=>'first_name DESC, last_name DESC', - * 'label'=>'Name' - * ) - * </pre> - * where 'user' is the name of the virtual attribute that specifies the full name of user - * (a compound attribute consisting of first name and last name of user). In this case, - * we have to use an array to define the virtual attribute with three elements: 'asc', - * 'desc' and 'label'. - * - * The above approach can also be used to declare virtual attributes that consist of relational - * attributes. For example, - * <pre> - * 'price'=>array( - * 'asc'=>'item.price', - * 'desc'=>'item.price DESC', - * 'label'=>'Item Price' + * ~~~ + * array( + * 'age', + * 'user' => array( + * 'asc' => array('first_name' => Sort::ASC, 'last_name' => Sort::ASC), + * 'desc' => array('first_name' => Sort::DESC, 'last_name' => Sort::DESC), + * 'default' => 'desc', + * ), * ) - * </pre> + * ~~~ * - * Note, the attribute name should not contain '-' or '.' characters because - * they are used as {@link separators}. + * In the above, two attributes are declared: "age" and "user". The "age" attribute is + * a simple attribute which is equivalent to the following: * - * Starting from version 1.1.3, an additional option named 'default' can be used in the virtual attribute - * declaration. This option specifies whether an attribute should be sorted in ascending or descending - * order upon user clicking the corresponding sort hyperlink if it is not currently sorted. The valid - * option values include 'asc' (default) and 'desc'. For example, - * <pre> - * 'price'=>array( - * 'asc'=>'item.price', - * 'desc'=>'item.price DESC', - * 'label'=>'Item Price', - * 'default'=>'desc', + * ~~~ + * 'age' => array( + * 'asc' => array('age' => Sort::ASC), + * 'desc' => array('age' => Sort::DESC), * ) - * </pre> + * ~~~ * - * Also starting from version 1.1.3, you can include a star ('*') element in this property so that - * all model attributes are available for sorting, in addition to those virtual attributes. For example, - * <pre> - * 'attributes'=>array( - * 'price'=>array( - * 'asc'=>'item.price', - * 'desc'=>'item.price DESC', - * 'label'=>'Item Price', - * 'default'=>'desc', - * ), - * '*', - * ) - * </pre> - * Note that when a name appears as both a model attribute and a virtual attribute, the position of - * the star element in the array determines which one takes precedence. In particular, if the star - * element is the first element in the array, the model attribute takes precedence; and if the star - * element is the last one, the virtual attribute takes precedence. + * The "user" attribute is a composite attribute: + * + * - The "user" key represents the attribute name which will appear in the URLs leading + * to sort actions. Attribute names cannot contain characters listed in [[separators]]. + * - The "asc" and "desc" elements specify how to sort by the attribute in ascending + * and descending orders, respectively. Their values represent the actual columns and + * the directions by which the data should be sorted by. + * - And the "default" element specifies if the attribute is not sorted currently, + * in which direction it should be sorted (the default value is ascending order). */ public $attributes = array(); /** - * @var string the name of the GET parameter that specifies which attributes to be sorted + * @var string the name of the parameter that specifies which attributes to be sorted * in which direction. Defaults to 'sort'. + * @see params */ public $sortVar = 'sort'; /** - * @var string the tag appeared in the GET parameter that indicates the attribute should be sorted + * @var string the tag appeared in the [[sortVar]] parameter that indicates the attribute should be sorted * in descending order. Defaults to 'desc'. */ public $descTag = 'desc'; /** - * @var mixed the default order that should be applied to the query criteria when - * the current request does not specify any sort. For example, 'name, create_time DESC' or - * 'UPPER(name)'. + * @var array the order that should be used when the current request does not specify any order. + * The array keys are attribute names and the array values are the corresponding sort directions. For example, * - * Starting from version 1.1.3, you can also specify the default order using an array. - * The array keys could be attribute names or virtual attribute names as declared in {@link attributes}, - * and the array values indicate whether the sorting of the corresponding attributes should - * be in descending order. For example, - * <pre> - * 'defaultOrder'=>array( - * 'price'=>CSort::SORT_DESC, + * ~~~ + * array( + * 'name' => Sort::ASC, + * 'create_time' => Sort::DESC, * ) - * </pre> - * `SORT_DESC` and `SORT_ASC` are available since 1.1.10. In earlier Yii versions you should use - * `true` and `false` respectively. + * ~~~ * - * Please note when using array to specify the default order, the corresponding attributes - * will be put into {@link directions} and thus affect how the sort links are rendered - * (e.g. an arrow may be displayed next to the currently active sort link). + * @see attributeOrders */ - public $defaultOrder; + public $defaults; /** - * @var string the route (controller ID and action ID) for generating the sorted contents. - * Defaults to empty string, meaning using the currently requested route. + * @var string the route of the controller action for displaying the sorted contents. + * If not set, it means using the currently requested route. */ - public $route = ''; + public $route; /** * @var array separators used in the generated URL. This must be an array consisting of * two elements. The first element specifies the character separating different * attributes, while the second element specifies the character separating attribute name - * and the corresponding sort direction. Defaults to array('-','.'). + * and the corresponding sort direction. Defaults to `array('-', '.')`. */ public $separators = array('-', '.'); /** - * @var array the additional GET parameters (name=>value) that should be used when generating sort URLs. - * Defaults to null, meaning using the currently available GET parameters. + * @var array parameters (name => value) that should be used to obtain the current sort directions + * and to create new sort URLs. If not set, $_GET will be used instead. + * + * The array element indexed by [[sortVar]] is considered to be the current sort directions. + * If the element does not exist, the [[defaults|default order]] will be used. + * + * @see sortVar + * @see defaults */ public $params; - private $_directions; - /** - * Constructor. - * @param string $modelClass the class name of data models that need to be sorted. - * This should be a child class of {@link CActiveRecord}. + * Returns the columns and their corresponding sort directions. + * @return array the columns (keys) and their corresponding sort directions (values). + * This can be passed to [[\yii\db\Query::orderBy()]] to construct a DB query. */ - public function __construct($modelClass = null) + public function getOrders() { - $this->modelClass = $modelClass; - } - - /** - * Modifies the query criteria by changing its {@link CDbCriteria::order} property. - * This method will use {@link directions} to determine which columns need to be sorted. - * They will be put in the ORDER BY clause. If the criteria already has non-empty {@link CDbCriteria::order} value, - * the new value will be appended to it. - * @param CDbCriteria $criteria the query criteria - */ - public function applyOrder($criteria) - { - $order = $this->getOrderBy(); - if (!empty($order)) { - if (!empty($criteria->order)) { - $criteria->order .= ', '; + $attributeOrders = $this->getAttributeOrders(); + $orders = array(); + foreach ($attributeOrders as $attribute => $direction) { + $definition = $this->getAttribute($attribute); + $columns = $definition[$direction === self::ASC ? 'asc' : 'desc']; + foreach ($columns as $name => $dir) { + $orders[$name] = $dir; } - $criteria->order .= $order; } + return $orders; } /** - * @return string the order-by columns represented by this sort object. - * This can be put in the ORDER BY clause of a SQL statement. - * @since 1.1.0 - */ - public function getOrderBy() - { - $directions = $this->getDirections(); - if (empty($directions)) { - return is_string($this->defaultOrder) ? $this->defaultOrder : ''; - } else { - if ($this->modelClass !== null) { - $schema = CActiveRecord::model($this->modelClass)->getDbConnection()->getSchema(); - } - $orders = array(); - foreach ($directions as $attribute => $descending) { - $definition = $this->resolveAttribute($attribute); - if (is_array($definition)) { - if ($descending) { - $orders[] = isset($definition['desc']) ? $definition['desc'] : $attribute . ' DESC'; - } else { - $orders[] = isset($definition['asc']) ? $definition['asc'] : $attribute; - } - } else { - if ($definition !== false) { - $attribute = $definition; - if (isset($schema)) { - if (($pos = strpos($attribute, '.')) !== false) { - $attribute = $schema->quoteTableName(substr($attribute, 0, $pos)) . '.' . $schema->quoteColumnName(substr($attribute, $pos + 1)); - } else { - $attribute = CActiveRecord::model($this->modelClass)->getTableAlias(true) . '.' . $schema->quoteColumnName($attribute); - } - } - $orders[] = $descending ? $attribute . ' DESC' : $attribute; - } - } - } - return implode(', ', $orders); - } - } - - /** - * Generates a hyperlink that can be clicked to cause sorting. - * @param string $attribute the attribute name. This must be the actual attribute name, not alias. - * If it is an attribute of a related AR object, the name should be prefixed with - * the relation name (e.g. 'author.name', where 'author' is the relation name). - * @param string $label the link label. If null, the label will be determined according - * to the attribute (see {@link resolveLabel}). + * Generates a hyperlink that links to the sort action to sort by the specified attribute. + * Based on the sort direction, the CSS class of the generated hyperlink will be appended + * with "asc" or "desc". + * @param string $attribute the attribute name by which the data should be sorted by. + * @param string $label the link label. Note that the label will not be HTML-encoded. * @param array $htmlOptions additional HTML attributes for the hyperlink tag * @return string the generated hyperlink */ - public function link($attribute, $label = null, $htmlOptions = array()) + public function link($attribute, $label, $htmlOptions = array()) { - if ($label === null) { - $label = $this->resolveLabel($attribute); - } - if (($definition = $this->resolveAttribute($attribute)) === false) { + if (($definition = $this->getAttribute($attribute)) === false) { return $label; } - $directions = $this->getDirections(); - if (isset($directions[$attribute])) { - $class = $directions[$attribute] ? 'desc' : 'asc'; + + if (($direction = $this->getAttributeOrder($attribute)) !== null) { + $class = $direction ? 'desc' : 'asc'; if (isset($htmlOptions['class'])) { $htmlOptions['class'] .= ' ' . $class; } else { $htmlOptions['class'] = $class; } - $descending = !$directions[$attribute]; - unset($directions[$attribute]); - } else { - if (is_array($definition) && isset($definition['default'])) { - $descending = $definition['default'] === 'desc'; - } else { - $descending = false; - } - } - - if ($this->multiSort) { - $directions = array_merge(array($attribute => $descending), $directions); - } else { - $directions = array($attribute => $descending); } - $url = $this->createUrl(\Yii::$application->getController(), $directions); + $url = $this->createUrl($attribute); - return $this->createLink($attribute, $label, $url, $htmlOptions); + return Html::link($label, $url, $htmlOptions); } - /** - * Resolves the attribute label for the specified attribute. - * This will invoke {@link CActiveRecord::getAttributeLabel} to determine what label to use. - * If the attribute refers to a virtual attribute declared in {@link attributes}, - * then the label given in the {@link attributes} will be returned instead. - * @param string $attribute the attribute name. - * @return string the attribute label - */ - public function resolveLabel($attribute) - { - $definition = $this->resolveAttribute($attribute); - if (is_array($definition)) { - if (isset($definition['label'])) { - return $definition['label']; - } - } else { - if (is_string($definition)) { - $attribute = $definition; - } - } - if ($this->modelClass !== null) { - return CActiveRecord::model($this->modelClass)->getAttributeLabel($attribute); - } else { - return $attribute; - } - } + private $_attributeOrders; /** * Returns the currently requested sort information. + * @param boolean $recalculate whether to recalculate the sort directions * @return array sort directions indexed by attribute names. - * Sort direction can be either CSort::SORT_ASC for ascending order or - * CSort::SORT_DESC for descending order. + * Sort direction can be either [[Sort::ASC]] for ascending order or + * [[Sort::DESC]] for descending order. */ - public function getDirections() + public function getAttributeOrders($recalculate = false) { - if ($this->_directions === null) { - $this->_directions = array(); - if (isset($_GET[$this->sortVar]) && is_string($_GET[$this->sortVar])) { - $attributes = explode($this->separators[0], $_GET[$this->sortVar]); + if ($this->_attributeOrders === null || $recalculate) { + $this->_attributeOrders = array(); + $params = $this->params === null ? $_GET : $this->params; + if (isset($params[$this->sortVar]) && is_scalar($params[$this->sortVar])) { + $attributes = explode($this->separators[0], $params[$this->sortVar]); foreach ($attributes as $attribute) { + $descending = false; if (($pos = strrpos($attribute, $this->separators[1])) !== false) { - $descending = substr($attribute, $pos + 1) === $this->descTag; - if ($descending) { + if ($descending = (substr($attribute, $pos + 1) === $this->descTag)) { $attribute = substr($attribute, 0, $pos); } - } else { - $descending = false; } - if (($this->resolveAttribute($attribute)) !== false) { - $this->_directions[$attribute] = $descending; - if (!$this->multiSort) { - return $this->_directions; + if (($this->getAttribute($attribute)) !== false) { + $this->_attributeOrders[$attribute] = $descending; + if (!$this->enableMultiSort) { + return $this->_attributeOrders; } } } } - if ($this->_directions === array() && is_array($this->defaultOrder)) { - $this->_directions = $this->defaultOrder; + if ($this->_attributeOrders === array() && is_array($this->defaults)) { + $this->_attributeOrders = $this->defaults; } } - return $this->_directions; + return $this->_attributeOrders; } /** * Returns the sort direction of the specified attribute in the current request. * @param string $attribute the attribute name - * @return mixed Sort direction of the attribute. Can be either CSort::SORT_ASC - * for ascending order or CSort::SORT_DESC for descending order. Value is null - * if the attribute doesn't need to be sorted. + * @return boolean|null Sort direction of the attribute. Can be either [[Sort::ASC]] + * for ascending order or [[Sort::DESC]] for descending order. Null is returned + * if the attribute is invalid or does not need to be sorted. */ - public function getDirection($attribute) + public function getAttributeOrder($attribute) { - $this->getDirections(); - return isset($this->_directions[$attribute]) ? $this->_directions[$attribute] : null; + $this->getAttributeOrders(); + return isset($this->_attributeOrders[$attribute]) ? $this->_attributeOrders[$attribute] : null; } /** - * Creates a URL that can lead to generating sorted data. - * @param CController $controller the controller that will be used to create the URL. - * @param array $directions the sort directions indexed by attribute names. - * The sort direction can be either CSort::SORT_ASC for ascending order or - * CSort::SORT_DESC for descending order. - * @return string the URL for sorting + * Creates a URL for sorting the data by the specified attribute. + * This method will consider the current sorting status given by [[attributeOrders]]. + * For example, if the current page already sorts the data by the specified attribute in ascending order, + * then the URL created will lead to a page that sorts the data by the specified attribute in descending order. + * @param string $attribute the attribute name + * @return string|boolean the URL for sorting. False if the attribute is invalid. + * @see attributeOrders + * @see params */ - public function createUrl($controller, $directions) + public function createUrl($attribute) { + if (($definition = $this->getAttribute($attribute)) === false) { + return false; + } + $directions = $this->getAttributeOrders(); + if (isset($directions[$attribute])) { + $descending = !$directions[$attribute]; + unset($directions[$attribute]); + } elseif (isset($definition['default'])) { + $descending = $definition['default'] === 'desc'; + } else { + $descending = false; + } + + if ($this->enableMultiSort) { + $directions = array_merge(array($attribute => $descending), $directions); + } else { + $directions = array($attribute => $descending); + } + $sorts = array(); foreach ($directions as $attribute => $descending) { $sorts[] = $descending ? $attribute . $this->separators[1] . $this->descTag : $attribute; } $params = $this->params === null ? $_GET : $this->params; $params[$this->sortVar] = implode($this->separators[0], $sorts); - return $controller->createUrl($this->route, $params); + $route = $this->route === null ? Yii::$app->controller->route : $this->route; + + return Yii::$app->getUrlManager()->createUrl($route, $params); } /** - * Returns the real definition of an attribute given its name. - * - * The resolution is based on {@link attributes} and {@link CActiveRecord::attributeNames}. - * <ul> - * <li>When {@link attributes} is an empty array, if the name refers to an attribute of {@link modelClass}, - * then the name is returned back.</li> - * <li>When {@link attributes} is not empty, if the name refers to an attribute declared in {@link attributes}, - * then the corresponding virtual attribute definition is returned. Starting from version 1.1.3, if {@link attributes} - * contains a star ('*') element, the name will also be used to match against all model attributes.</li> - * <li>In all other cases, false is returned, meaning the name does not refer to a valid attribute.</li> - * </ul> - * @param string $attribute the attribute name that the user requests to sort on - * @return mixed the attribute name or the virtual attribute definition. False if the attribute cannot be sorted. + * Returns the attribute definition of the specified name. + * @param string $name the attribute name + * @return array|boolean the sort definition (column names => sort directions). + * False is returned if the attribute cannot be sorted. + * @see attributes */ - public function resolveAttribute($attribute) + public function getAttribute($name) { - if ($this->attributes !== array()) { - $attributes = $this->attributes; + if (isset($this->attributes[$name])) { + return $this->attributes[$name]; + } elseif (in_array($name, $this->attributes, true)) { + return array( + 'asc' => array($name => self::ASC), + 'desc' => array($name => self::DESC), + ); } else { - if ($this->modelClass !== null) { - $attributes = CActiveRecord::model($this->modelClass)->attributes(); - } else { - return false; - } + return false; } - foreach ($attributes as $name => $definition) { - if (is_string($name)) { - if ($name === $attribute) { - return $definition; - } - } else { - if ($definition === '*') { - if ($this->modelClass !== null && CActiveRecord::model($this->modelClass)->hasAttribute($attribute)) { - return $attribute; - } - } else { - if ($definition === $attribute) { - return $attribute; - } - } - } - } - return false; - } - - /** - * Creates a hyperlink based on the given label and URL. - * You may override this method to customize the link generation. - * @param string $attribute the name of the attribute that this link is for - * @param string $label the label of the hyperlink - * @param string $url the URL - * @param array $htmlOptions additional HTML options - * @return string the generated hyperlink - */ - protected function createLink($attribute, $label, $url, $htmlOptions) - { - return CHtml::link($label, $url, $htmlOptions); } } \ No newline at end of file diff --git a/framework/web/Theme.php b/framework/web/Theme.php deleted file mode 100644 index 5dcd601..0000000 --- a/framework/web/Theme.php +++ /dev/null @@ -1,141 +0,0 @@ -<?php -/** - * CTheme class file. - * - * @author Qiang Xue <qiang.xue@gmail.com> - * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008-2011 Yii Software LLC - * @license http://www.yiiframework.com/license/ - */ - -/** - * CTheme represents an application theme. - * - * @property string $name Theme name. - * @property string $baseUrl The relative URL to the theme folder (without ending slash). - * @property string $basePath The file path to the theme folder. - * @property string $viewPath The path for controller views. Defaults to 'ThemeRoot/views'. - * @property string $systemViewPath The path for system views. Defaults to 'ThemeRoot/views/system'. - * @property string $skinPath The path for widget skins. Defaults to 'ThemeRoot/views/skins'. - * - * @author Qiang Xue <qiang.xue@gmail.com> - * @version $Id$ - * @package system.web - * @since 1.0 - */ -class CTheme extends CComponent -{ - private $_name; - private $_basePath; - private $_baseUrl; - - /** - * Constructor. - * @param string $name name of the theme - * @param string $basePath base theme path - * @param string $baseUrl base theme URL - */ - public function __construct($name,$basePath,$baseUrl) - { - $this->_name=$name; - $this->_baseUrl=$baseUrl; - $this->_basePath=$basePath; - } - - /** - * @return string theme name - */ - public function getName() - { - return $this->_name; - } - - /** - * @return string the relative URL to the theme folder (without ending slash) - */ - public function getBaseUrl() - { - return $this->_baseUrl; - } - - /** - * @return string the file path to the theme folder - */ - public function getBasePath() - { - return $this->_basePath; - } - - /** - * @return string the path for controller views. Defaults to 'ThemeRoot/views'. - */ - public function getViewPath() - { - return $this->_basePath.DIRECTORY_SEPARATOR.'views'; - } - - /** - * @return string the path for system views. Defaults to 'ThemeRoot/views/system'. - */ - public function getSystemViewPath() - { - return $this->getViewPath().DIRECTORY_SEPARATOR.'system'; - } - - /** - * @return string the path for widget skins. Defaults to 'ThemeRoot/views/skins'. - * @since 1.1 - */ - public function getSkinPath() - { - return $this->getViewPath().DIRECTORY_SEPARATOR.'skins'; - } - - /** - * Finds the view file for the specified controller's view. - * @param CController $controller the controller - * @param string $viewName the view name - * @return string the view file path. False if the file does not exist. - */ - public function getViewFile($controller,$viewName) - { - $moduleViewPath=$this->getViewPath(); - if(($module=$controller->getModule())!==null) - $moduleViewPath.='/'.$module->getId(); - return $controller->resolveViewFile($viewName,$this->getViewPath().'/'.$controller->getUniqueId(),$this->getViewPath(),$moduleViewPath); - } - - /** - * Finds the layout file for the specified controller's layout. - * @param CController $controller the controller - * @param string $layoutName the layout name - * @return string the layout file path. False if the file does not exist. - */ - public function getLayoutFile($controller,$layoutName) - { - $moduleViewPath=$basePath=$this->getViewPath(); - $module=$controller->getModule(); - if(empty($layoutName)) - { - while($module!==null) - { - if($module->layout===false) - return false; - if(!empty($module->layout)) - break; - $module=$module->getParentModule(); - } - if($module===null) - $layoutName=\Yii::$application->layout; - else - { - $layoutName=$module->layout; - $moduleViewPath.='/'.$module->getId(); - } - } - else if($module!==null) - $moduleViewPath.='/'.$module->getId(); - - return $controller->resolveViewFile($layoutName,$moduleViewPath.'/layouts',$basePath,$moduleViewPath); - } -} diff --git a/framework/web/UrlManager.php b/framework/web/UrlManager.php new file mode 100644 index 0000000..459e8e8 --- /dev/null +++ b/framework/web/UrlManager.php @@ -0,0 +1,251 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\web; + +use Yii; +use yii\base\Component; +use yii\caching\Cache; + +/** + * UrlManager handles HTTP request parsing and creation of URLs based on a set of rules. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class UrlManager extends Component +{ + /** + * @var boolean whether to enable pretty URLs. Instead of putting all parameters in the query + * string part of a URL, pretty URLs allow using path info to represent some of the parameters + * and can thus produce more user-friendly URLs, such as "/news/Yii-is-released", instead of + * "/index.php?r=news/view&id=100". + */ + public $enablePrettyUrl = false; + /** + * @var array the rules for creating and parsing URLs when [[enablePrettyUrl]] is true. + * This property is used only if [[enablePrettyUrl]] is true. Each element in the array + * is the configuration of creating a single URL rule whose class by default is [[defaultRuleClass]]. + * If you modify this property after the UrlManager object is created, make sure + * you populate the array with rule objects instead of rule configurations. + */ + public $rules = array(); + /** + * @var string the URL suffix used when in 'path' format. + * For example, ".html" can be used so that the URL looks like pointing to a static HTML page. + * This property is used only if [[enablePrettyUrl]] is true. + */ + public $suffix; + /** + * @var boolean whether to show entry script name in the constructed URL. Defaults to true. + * This property is used only if [[enablePrettyUrl]] is true. + */ + public $showScriptName = true; + /** + * @var string the GET variable name for route. This property is used only if [[enablePrettyUrl]] is false. + */ + public $routeVar = 'r'; + /** + * @var Cache|string the cache object or the application component ID of the cache object. + * Compiled URL rules will be cached through this cache object, if it is available. + * + * After the UrlManager object is created, if you want to change this property, + * you should only assign it with a cache object. + * Set this property to null if you do not want to cache the URL rules. + */ + public $cache = 'cache'; + /** + * @var string the default class name for creating URL rule instances + * when it is not specified in [[rules]]. + */ + public $defaultRuleClass = 'yii\web\UrlRule'; + + private $_baseUrl; + private $_hostInfo; + + + /** + * Initializes UrlManager. + */ + public function init() + { + parent::init(); + if (is_string($this->cache)) { + $this->cache = Yii::$app->getComponent($this->cache); + } + $this->compileRules(); + } + + /** + * Parses the URL rules. + */ + protected function compileRules() + { + if (!$this->enablePrettyUrl || $this->rules === array()) { + return; + } + if ($this->cache instanceof Cache) { + $key = $this->cache->buildKey(__CLASS__); + $hash = md5(json_encode($this->rules)); + if (($data = $this->cache->get($key)) !== false && isset($data[1]) && $data[1] === $hash) { + $this->rules = $data[0]; + return; + } + } + + foreach ($this->rules as $i => $rule) { + if (!isset($rule['class'])) { + $rule['class'] = $this->defaultRuleClass; + } + $this->rules[$i] = Yii::createObject($rule); + } + + if ($this->cache instanceof Cache) { + $this->cache->set($key, array($this->rules, $hash)); + } + } + + /** + * Parses the user request. + * @param Request $request the request component + * @return array|boolean the route and the associated parameters. The latter is always empty + * if [[enablePrettyUrl]] is false. False is returned if the current request cannot be successfully parsed. + */ + public function parseRequest($request) + { + if ($this->enablePrettyUrl) { + $pathInfo = $request->pathInfo; + /** @var $rule UrlRule */ + foreach ($this->rules as $rule) { + if (($result = $rule->parseRequest($this, $request)) !== false) { + return $result; + } + } + + $suffix = (string)$this->suffix; + if ($suffix !== '' && $suffix !== '/' && $pathInfo !== '') { + $n = strlen($this->suffix); + if (substr($pathInfo, -$n) === $this->suffix) { + $pathInfo = substr($pathInfo, 0, -$n); + if ($pathInfo === '') { + // suffix alone is not allowed + return false; + } + } else { + // suffix doesn't match + return false; + } + } + + return array($pathInfo, array()); + } else { + $route = $request->getParam($this->routeVar); + if (is_array($route)) { + $route = ''; + } + return array((string)$route, array()); + } + } + + /** + * Creates a URL using the given route and parameters. + * The URL created is a relative one. Use [[createAbsoluteUrl()]] to create an absolute URL. + * @param string $route the route + * @param array $params the parameters (name-value pairs) + * @return string the created URL + */ + public function createUrl($route, $params = array()) + { + $anchor = isset($params['#']) ? '#' . $params['#'] : ''; + unset($params['#']); + + $route = trim($route, '/'); + $baseUrl = $this->getBaseUrl(); + + if ($this->enablePrettyUrl) { + /** @var $rule UrlRule */ + foreach ($this->rules as $rule) { + if (($url = $rule->createUrl($this, $route, $params)) !== false) { + return rtrim($baseUrl, '/') . '/' . $url . $anchor; + } + } + + if ($this->suffix !== null) { + $route .= $this->suffix; + } + if ($params !== array()) { + $route .= '?' . http_build_query($params); + } + return rtrim($baseUrl, '/') . '/' . $route . $anchor; + } else { + $url = $baseUrl . '?' . $this->routeVar . '=' . $route; + if ($params !== array()) { + $url .= '&' . http_build_query($params); + } + return $url; + } + } + + /** + * Creates an absolute URL using the given route and parameters. + * This method prepends the URL created by [[createUrl()]] with the [[hostInfo]]. + * @param string $route the route + * @param array $params the parameters (name-value pairs) + * @return string the created URL + * @see createUrl() + */ + public function createAbsoluteUrl($route, $params = array()) + { + return $this->getHostInfo() . $this->createUrl($route, $params); + } + + /** + * Returns the base URL that is used by [[createUrl()]] to prepend URLs it creates. + * It defaults to [[Request::scriptUrl]] if [[showScriptName]] is true or [[enablePrettyUrl]] is false; + * otherwise, it defaults to [[Request::baseUrl]]. + * @return string the base URL that is used by [[createUrl()]] to prepend URLs it creates. + */ + public function getBaseUrl() + { + if ($this->_baseUrl === null) { + /** @var $request \yii\web\Request */ + $request = Yii::$app->getRequest(); + $this->_baseUrl = $this->showScriptName || !$this->enablePrettyUrl ? $request->getScriptUrl() : $request->getBaseUrl(); + } + return $this->_baseUrl; + } + + /** + * Sets the base URL that is used by [[createUrl()]] to prepend URLs it creates. + * @param string $value the base URL that is used by [[createUrl()]] to prepend URLs it creates. + */ + public function setBaseUrl($value) + { + $this->_baseUrl = $value; + } + + /** + * Returns the host info that is used by [[createAbsoluteUrl()]] to prepend URLs it creates. + * @return string the host info (e.g. "http://www.example.com") that is used by [[createAbsoluteUrl()]] to prepend URLs it creates. + */ + public function getHostInfo() + { + if ($this->_hostInfo === null) { + $this->_hostInfo = Yii::$app->getRequest()->getHostInfo(); + } + return $this->_hostInfo; + } + + /** + * Sets the host info that is used by [[createAbsoluteUrl()]] to prepend URLs it creates. + * @param string $value the host info (e.g. "http://www.example.com") that is used by [[createAbsoluteUrl()]] to prepend URLs it creates. + */ + public function setHostInfo($value) + { + $this->_hostInfo = rtrim($value, '/'); + } +} diff --git a/framework/web/UrlRule.php b/framework/web/UrlRule.php new file mode 100644 index 0000000..d9cb4fd --- /dev/null +++ b/framework/web/UrlRule.php @@ -0,0 +1,283 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\web; + +use yii\base\Object; +use yii\base\InvalidConfigException; + +/** + * UrlRule represents a rule used for parsing and generating URLs. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class UrlRule extends Object +{ + /** + * Set [[mode]] with this value to mark that this rule is for URL parsing only + */ + const PARSING_ONLY = 1; + /** + * Set [[mode]] with this value to mark that this rule is for URL creation only + */ + const CREATION_ONLY = 2; + + /** + * @var string regular expression used to parse a URL + */ + public $pattern; + /** + * @var string the route to the controller action + */ + public $route; + /** + * @var array the default GET parameters (name=>value) that this rule provides. + * When this rule is used to parse the incoming request, the values declared in this property + * will be injected into $_GET. + */ + public $defaults = array(); + /** + * @var string the URL suffix used for this rule. + * For example, ".html" can be used so that the URL looks like pointing to a static HTML page. + * If not, the value of [[UrlManager::suffix]] will be used. + */ + public $suffix; + /** + * @var string|array the HTTP verb (e.g. GET, POST, DELETE) that this rule should match. + * Use array to represent multiple verbs that this rule may match. + * If this property is not set, the rule can match any verb. + * Note that this property is only used when parsing a request. It is ignored for URL creation. + */ + public $verb; + /** + * @var integer a value indicating if this rule should be used for both request parsing and URL creation, + * parsing only, or creation only. + * If not set or 0, it means the rule is both request parsing and URL creation. + * If it is [[PARSING_ONLY]], the rule is for request parsing only. + * If it is [[CREATION_ONLY]], the rule is for URL creation only. + */ + public $mode; + + /** + * @var string the template for generating a new URL. This is derived from [[pattern]] and is used in generating URL. + */ + private $_template; + /** + * @var string the regex for matching the route part. This is used in generating URL. + */ + private $_routeRule; + /** + * @var array list of regex for matching parameters. This is used in generating URL. + */ + private $_paramRules = array(); + /** + * @var array list of parameters used in the route. + */ + private $_routeParams = array(); + + /** + * Initializes this rule. + */ + public function init() + { + if ($this->pattern === null) { + throw new InvalidConfigException('UrlRule::pattern must be set.'); + } + if ($this->route === null) { + throw new InvalidConfigException('UrlRule::route must be set.'); + } + if ($this->verb !== null) { + if (is_array($this->verb)) { + foreach ($this->verb as $i => $verb) { + $this->verb[$i] = strtoupper($verb); + } + } else { + $this->verb = array(strtoupper($this->verb)); + } + } + + $this->pattern = trim($this->pattern, '/'); + if ($this->pattern === '') { + $this->_template = ''; + $this->pattern = '#^$#u'; + return; + } else { + $this->pattern = '/' . $this->pattern . '/'; + } + + $this->route = trim($this->route, '/'); + if (strpos($this->route, '<') !== false && preg_match_all('/<(\w+)>/', $this->route, $matches)) { + foreach ($matches[1] as $name) { + $this->_routeParams[$name] = "<$name>"; + } + } + + $tr = $tr2 = array(); + if (preg_match_all('/<(\w+):?([^>]+)?>/', $this->pattern, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER)) { + foreach ($matches as $match) { + $name = $match[1][0]; + $pattern = isset($match[2][0]) ? $match[2][0] : '[^\/]+'; + if (isset($this->defaults[$name])) { + $length = strlen($match[0][0]); + $offset = $match[0][1]; + if ($this->pattern[$offset - 1] === '/' && $this->pattern[$offset + $length] === '/') { + $tr["/<$name>"] = "(/(?P<$name>$pattern))?"; + } else { + $tr["<$name>"] = "(?P<$name>$pattern)?"; + } + } else { + $tr["<$name>"] = "(?P<$name>$pattern)"; + } + if (isset($this->_routeParams[$name])) { + $tr2["<$name>"] = "(?P<$name>$pattern)"; + } else { + $this->_paramRules[$name] = $pattern === '[^\/]+' ? '' : "#^$pattern$#"; + } + } + } + + $this->_template = preg_replace('/<(\w+):?([^>]+)?>/', '<$1>', $this->pattern); + $this->pattern = '#^' . trim(strtr($this->_template, $tr), '/') . '$#u'; + + if ($this->_routeParams !== array()) { + $this->_routeRule = '#^' . strtr($this->route, $tr2) . '$#u'; + } + } + + /** + * Parses the given request and returns the corresponding route and parameters. + * @param UrlManager $manager the URL manager + * @param Request $request the request component + * @return array|boolean the parsing result. The route and the parameters are returned as an array. + * If false, it means this rule cannot be used to parse this path info. + */ + public function parseRequest($manager, $request) + { + if ($this->mode === self::CREATION_ONLY) { + return false; + } + + if ($this->verb !== null && !in_array($request->verb, $this->verb, true)) { + return false; + } + + $pathInfo = $request->pathInfo; + $suffix = (string)($this->suffix === null ? $manager->suffix : $this->suffix); + if ($suffix !== '' && $pathInfo !== '') { + $n = strlen($suffix); + if (substr($pathInfo, -$n) === $suffix) { + $pathInfo = substr($pathInfo, 0, -$n); + if ($pathInfo === '') { + // suffix alone is not allowed + return false; + } + } elseif ($suffix !== '/') { + // we allow the ending '/' to be optional if it is a suffix + return false; + } + } + + if (!preg_match($this->pattern, $pathInfo, $matches)) { + return false; + } + foreach ($this->defaults as $name => $value) { + if (!isset($matches[$name]) || $matches[$name] === '') { + $matches[$name] = $value; + } + } + $params = $this->defaults; + $tr = array(); + foreach ($matches as $name => $value) { + if (isset($this->_routeParams[$name])) { + $tr[$this->_routeParams[$name]] = $value; + unset($params[$name]); + } elseif (isset($this->_paramRules[$name])) { + $params[$name] = $value; + } + } + if ($this->_routeRule !== null) { + $route = strtr($this->route, $tr); + } else { + $route = $this->route; + } + return array($route, $params); + } + + /** + * Creates a URL according to the given route and parameters. + * @param UrlManager $manager the URL manager + * @param string $route the route. It should not have slashes at the beginning or the end. + * @param array $params the parameters + * @return string|boolean the created URL, or false if this rule cannot be used for creating this URL. + */ + public function createUrl($manager, $route, $params) + { + if ($this->mode === self::PARSING_ONLY) { + return false; + } + + $tr = array(); + + // match the route part first + if ($route !== $this->route) { + if ($this->_routeRule !== null && preg_match($this->_routeRule, $route, $matches)) { + foreach ($this->_routeParams as $name => $token) { + if (isset($this->defaults[$name]) && strcmp($this->defaults[$name], $matches[$name]) === 0) { + $tr[$token] = ''; + } else { + $tr[$token] = $matches[$name]; + } + } + } else { + return false; + } + } + + // match default params + // if a default param is not in the route pattern, its value must also be matched + foreach ($this->defaults as $name => $value) { + if (isset($this->_routeParams[$name])) { + continue; + } + if (!isset($params[$name])) { + return false; + } elseif (strcmp($params[$name], $value) === 0) { // strcmp will do string conversion automatically + unset($params[$name]); + if (isset($this->_paramRules[$name])) { + $tr["<$name>"] = ''; + } + } elseif (!isset($this->_paramRules[$name])) { + return false; + } + } + + // match params in the pattern + foreach ($this->_paramRules as $name => $rule) { + if (isset($params[$name]) && ($rule === '' || preg_match($rule, $params[$name]))) { + $tr["<$name>"] = urlencode($params[$name]); + unset($params[$name]); + } elseif (!isset($this->defaults[$name]) || isset($params[$name])) { + return false; + } + } + + $url = trim(strtr($this->_template, $tr), '/'); + if (strpos($url, '//') !== false) { + $url = preg_replace('#/+#', '/', $url); + } + + if ($url !== '') { + $url .= ($this->suffix === null ? $manager->suffix : $this->suffix); + } + + if ($params !== array()) { + $url .= '?' . http_build_query($params); + } + return $url; + } +} diff --git a/framework/web/User.php b/framework/web/User.php new file mode 100644 index 0000000..2326a10 --- /dev/null +++ b/framework/web/User.php @@ -0,0 +1,547 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\web; + +use Yii; +use yii\base\Component; +use yii\base\InvalidConfigException; + +/** + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class User extends Component +{ + const ID_VAR = '__id'; + const AUTH_EXPIRE_VAR = '__expire'; + + const EVENT_BEFORE_LOGIN = 'beforeLogin'; + const EVENT_AFTER_LOGIN = 'afterLogin'; + const EVENT_BEFORE_LOGOUT = 'beforeLogout'; + const EVENT_AFTER_LOGOUT = 'afterLogout'; + + /** + * @var string the class name of the [[identity]] object. + */ + public $identityClass; + /** + * @var boolean whether to enable cookie-based login. Defaults to false. + */ + public $enableAutoLogin = false; + /** + * @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'); + /** + * @var array the configuration of the identity cookie. This property is used only when [[enableAutoLogin]] is true. + * @see Cookie + */ + public $identityCookie = array('name' => '__identity'); + /** + * @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 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 enableAutoLogin + * @since 1.1.0 + */ + public $autoRenewCookie = false; + /** + * @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 + * a problem if the ajax call expects the result to be a json array or a predefined string, as the login page is ignored in that case. + * To solve this, set this property to the desired return value. + * + * If this property is set, this value will be returned as the result of the ajax call in case that the user session has expired. + * @since 1.1.9 + * @see loginRequired + */ + public $loginRequiredAjaxResponse; + + + public $stateVar = '__states'; + + /** + * Initializes the application component. + */ + public function init() + { + parent::init(); + + if ($this->enableAutoLogin && !isset($this->identityCookie['name'])) { + throw new InvalidConfigException('User::identityCookie must contain the "name" element.'); + } + + Yii::$app->getSession()->open(); + + $this->renewAuthStatus(); + + if ($this->enableAutoLogin) { + if ($this->getIsGuest()) { + $this->loginByCookie(); + } elseif ($this->autoRenewCookie) { + $this->renewIdentityCookie(); + } + } + } + + /** + * @var Identity the identity object associated with the currently logged user. + */ + private $_identity = false; + + public function getIdentity() + { + if ($this->_identity === false) { + $id = $this->getId(); + if ($id === null) { + $this->_identity = null; + } else { + /** @var $class Identity */ + $class = $this->identityClass; + $this->_identity = $class::findIdentity($this->getId()); + } + } + return $this->_identity; + } + + public function setIdentity($identity) + { + $this->switchIdentity($identity); + } + + /** + * Logs in a user. + * + * The user identity information will be saved in storage that is + * persistent during the user session. By default, the storage is simply + * 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 enableAutoLogin} to true + * if you want to allow user to be authenticated based on the cookie information. + * + * @param Identity $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 enableAutoLogin} + * must be set true, otherwise an exception will be thrown. + * @return boolean whether the user is logged in + */ + public function login($identity, $duration = 0) + { + if ($this->beforeLogin($identity, false)) { + $this->switchIdentity($identity); + if ($duration > 0 && $this->enableAutoLogin) { + $this->saveIdentityCookie($identity, $duration); + } + $this->afterLogin($identity, false); + } + return !$this->getIsGuest(); + } + + /** + * Populates the current user object with the information obtained from cookie. + * 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 saveIdentityCookie + */ + protected function loginByCookie() + { + $name = $this->identityCookie['name']; + $value = Yii::$app->getRequest()->getCookies()->getValue($name); + if ($value !== null) { + $data = json_decode($value, true); + if (count($data) === 3 && isset($data[0], $data[1], $data[2])) { + list ($id, $authKey, $duration) = $data; + /** @var $class Identity */ + $class = $this->identityClass; + $identity = $class::findIdentity($id); + if ($identity !== null && $identity->validateAuthKey($authKey) && $this->beforeLogin($identity, true)) { + $this->switchIdentity($identity); + if ($this->autoRenewCookie) { + $this->saveIdentityCookie($identity, $duration); + } + $this->afterLogin($identity, true); + } + } + } + } + + /** + * Logs out the current user. + * This will remove authentication-related session data. + * If the parameter is true, the whole session will be destroyed as well. + * @param boolean $destroySession whether to destroy the whole session. Defaults to true. If false, + * then {@link clearStates} will be called, which removes only the data stored via {@link setState}. + */ + public function logout($destroySession = true) + { + $identity = $this->getIdentity(); + if ($identity !== null && $this->beforeLogout($identity)) { + $this->switchIdentity(null); + if ($this->enableAutoLogin) { + Yii::$app->getResponse()->getCookies()->remove(new Cookie($this->identityCookie)); + } + if ($destroySession) { + Yii::$app->getSession()->destroy(); + } + $this->afterLogout($identity); + } + } + + /** + * Returns a value indicating whether the user is a guest (not authenticated). + * @return boolean whether the current user is a guest. + */ + public function getIsGuest() + { + return $this->getIdentity() === null; + } + + /** + * Returns a value that uniquely represents the user. + * @return mixed the unique identifier for the user. If null, it means the user is a guest. + */ + public function getId() + { + return $this->getState(static::ID_VAR); + } + + /** + * @param mixed $value the unique identifier for the user. If null, it means the user is a guest. + */ + public function setId($value) + { + $this->setState(static::ID_VAR, $value); + } + + /** + * Returns the URL that the user should be redirected to after successful login. + * This property is usually used by the login action. If the login is successful, + * the action should read this property and use it to redirect the user browser. + * @param string $defaultUrl the default return URL in case it was not set previously. If this is null, + * the application entry URL will be considered as the default return URL. + * @return string the URL that the user should be redirected to after login. + * @see loginRequired + */ + public function getReturnUrl($defaultUrl = null) + { + if ($defaultUrl === null) { + $defaultReturnUrl = Yii::app()->getUrlManager()->showScriptName ? Yii::app()->getRequest()->getScriptUrl() : Yii::app()->getRequest()->getBaseUrl() . '/'; + } else { + $defaultReturnUrl = CHtml::normalizeUrl($defaultUrl); + } + return $this->getState('__returnUrl', $defaultReturnUrl); + } + + /** + * @param string $value the URL that the user should be redirected to after login. + */ + public function setReturnUrl($value) + { + $this->setState('__returnUrl', $value); + } + + /** + * Redirects the user browser to the login page. + * Before the redirection, the current URL (if it's not an AJAX url) will be + * kept in {@link returnUrl} so that the user browser may be redirected back + * to the current page after successful login. Make sure you set {@link loginUrl} + * so that the user browser can be redirected to the specified login URL after + * calling this method. + * After calling this method, the current request processing will be terminated. + */ + public function loginRequired() + { + $app = Yii::app(); + $request = $app->getRequest(); + + if (!$request->getIsAjaxRequest()) { + $this->setReturnUrl($request->getUrl()); + } elseif (isset($this->loginRequiredAjaxResponse)) { + echo $this->loginRequiredAjaxResponse; + Yii::app()->end(); + } + + if (($url = $this->loginUrl) !== null) { + if (is_array($url)) { + $route = isset($url[0]) ? $url[0] : $app->defaultController; + $url = $app->createUrl($route, array_splice($url, 1)); + } + $request->redirect($url); + } else { + throw new CHttpException(403, Yii::t('yii', 'Login Required')); + } + } + + /** + * This method is called before logging in a user. + * You may override this method to provide additional security check. + * For example, when the login is cookie-based, you may want to verify + * that the user ID together with a random token in the states can be found + * in the database. This will prevent hackers from faking arbitrary + * identity cookies even if they crack down the server private key. + * @param mixed $id the user ID. This is the same as returned by {@link getId()}. + * @param array $states a set of name-value pairs that are provided by the user identity. + * @param boolean $fromCookie whether the login is based on cookie + * @return boolean whether the user should be logged in + */ + protected function beforeLogin($identity, $fromCookie) + { + $event = new UserEvent(array( + 'identity' => $identity, + 'fromCookie' => $fromCookie, + )); + $this->trigger(self::EVENT_BEFORE_LOGIN, $event); + return $event->isValid; + } + + /** + * This method is called after the user is successfully logged in. + * You may override this method to do some postprocessing (e.g. log the user + * login IP and time; load the user permission information). + * @param boolean $fromCookie whether the login is based on cookie. + */ + protected function afterLogin($identity, $fromCookie) + { + $this->trigger(self::EVENT_AFTER_LOGIN, new UserEvent(array( + 'identity' => $identity, + 'fromCookie' => $fromCookie, + ))); + } + + /** + * This method is invoked when calling {@link logout} to log out a user. + * If this method return false, the logout action will be cancelled. + * You may override this method to provide additional check before + * logging out a user. + * @return boolean whether to log out the user + */ + protected function beforeLogout($identity) + { + $event = new UserEvent(array( + 'identity' => $identity, + )); + $this->trigger(self::EVENT_BEFORE_LOGOUT, $event); + return $event->isValid; + } + + /** + * This method is invoked right after a user is logged out. + * You may override this method to do some extra cleanup work for the user. + */ + protected function afterLogout($identity) + { + $this->trigger(self::EVENT_AFTER_LOGOUT, new UserEvent(array( + 'identity' => $identity, + ))); + } + + + /** + * Renews the identity cookie. + * This method will set the expiration time of the identity cookie to be the current time + * plus the originally specified cookie duration. + */ + protected function renewIdentityCookie() + { + $name = $this->identityCookie['name']; + $value = Yii::$app->getRequest()->getCookies()->getValue($name); + if ($value !== null) { + $data = json_decode($value, true); + if (is_array($data) && isset($data[2])) { + $cookie = new Cookie($this->identityCookie); + $cookie->value = $value; + $cookie->expire = time() + (int)$data[2]; + Yii::$app->getResponse()->getCookies()->add($cookie); + } + } + } + + /** + * Saves necessary user data into a cookie. + * 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 Identity $identity + * @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. + * @see loginByCookie + */ + protected function saveIdentityCookie($identity, $duration) + { + $cookie = new Cookie($this->identityCookie); + $cookie->value = json_encode(array( + $identity->getId(), + $identity->getAuthKey(), + $duration, + )); + $cookie->expire = time() + $duration; + Yii::$app->getResponse()->getCookies()->add($cookie); + } + + /** + * 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 + * identity information. Derived classes may override this method + * by retrieving additional user-related information. Make sure the + * parent implementation is called first. + * @param Identity $identity a unique identifier for the user + */ + protected function switchIdentity($identity) + { + Yii::$app->getSession()->regenerateID(true); + $this->setIdentity($identity); + if ($identity instanceof Identity) { + $this->setId($identity->getId()); + if ($this->authTimeout !== null) { + $this->setState(self::AUTH_EXPIRE_VAR, time() + $this->authTimeout); + } + } else { + $this->removeAllStates(); + } + } + + /** + * Updates the authentication status according to {@link authTimeout}. + * If the user has been inactive for {@link authTimeout} seconds, + * he will be automatically logged out. + */ + protected function renewAuthStatus() + { + if ($this->authTimeout !== null && !$this->getIsGuest()) { + $expire = $this->getState(self::AUTH_EXPIRE_VAR); + if ($expire !== null && $expire < time()) { + $this->logout(false); + } else { + $this->setState(self::AUTH_EXPIRE_VAR, time() + $this->authTimeout); + } + } + } + + /** + * Returns a user state. + * A user state is a session data item associated with the current user. + * If the user logs out, all his/her user states will be removed. + * @param string $key the key identifying the state + * @param mixed $defaultValue value to be returned if the state does not exist. + * @return mixed the state + */ + public function getState($key, $defaultValue = null) + { + $manifest = isset($_SESSION[$this->stateVar]) ? $_SESSION[$this->stateVar] : null; + if (is_array($manifest) && isset($manifest[$key], $_SESSION[$key])) { + return $_SESSION[$key]; + } else { + return $defaultValue; + } + } + + /** + * Returns all user states. + * @return array states (key => state). + */ + public function getAllStates() + { + $manifest = isset($_SESSION[$this->stateVar]) ? $_SESSION[$this->stateVar] : null; + $states = array(); + if (is_array($manifest)) { + foreach (array_keys($manifest) as $key) { + if (isset($_SESSION[$key])) { + $states[$key] = $_SESSION[$key]; + } + } + } + return $states; + } + + /** + * Stores a user state. + * A user state is a session data item associated with the current user. + * If the user logs out, all his/her user states will be removed. + * @param string $key the key identifying the state. Note that states + * 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 state + */ + public function setState($key, $value) + { + $manifest = isset($_SESSION[$this->stateVar]) ? $_SESSION[$this->stateVar] : array(); + $manifest[$value] = true; + $_SESSION[$key] = $value; + $_SESSION[$this->stateVar] = $manifest; + } + + /** + * Removes a user state. + * If the user logs out, all his/her user states will be removed automatically. + * @param string $key the key identifying the state. Note that states + * 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 state. Null if the state does not exist. + */ + public function removeState($key) + { + $manifest = isset($_SESSION[$this->stateVar]) ? $_SESSION[$this->stateVar] : null; + if (is_array($manifest) && isset($manifest[$key], $_SESSION[$key])) { + $value = $_SESSION[$key]; + } else { + $value = null; + } + unset($_SESSION[$this->stateVar][$key], $_SESSION[$key]); + return $value; + } + + /** + * Removes all states. + * If the user logs out, all his/her user states will be removed automatically + * without the need to call this method manually. + * + * Note that states 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 removeAllStates() + { + $manifest = isset($_SESSION[$this->stateVar]) ? $_SESSION[$this->stateVar] : null; + if (is_array($manifest)) { + foreach (array_keys($manifest) as $key) { + unset($_SESSION[$key]); + } + } + unset($_SESSION[$this->stateVar]); + } + + /** + * Returns a value indicating whether there is a state associated with the specified key. + * @param string $key key identifying the state + * @return boolean whether the specified state exists + */ + public function hasState($key) + { + return $this->getState($key) !== null; + } +} diff --git a/framework/web/UserEvent.php b/framework/web/UserEvent.php new file mode 100644 index 0000000..3a8723a --- /dev/null +++ b/framework/web/UserEvent.php @@ -0,0 +1,34 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\web; + +use yii\base\Event; + +/** + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class UserEvent extends Event +{ + /** + * @var Identity the identity object associated with this event + */ + public $identity; + /** + * @var boolean whether the login is cookie-based. This property is only meaningful + * for [[User::EVENT_BEFORE_LOGIN]] and [[User::EVENT_AFTER_LOGIN]] events. + */ + public $fromCookie; + /** + * @var boolean whether the login or logout should proceed. + * Event handlers may modify this property to determine whether the login or logout should proceed. + * This property is only meaningful for [[User::EVENT_BEFORE_LOGIN]] and [[User::EVENT_BEFORE_LOGOUT]] events. + */ + public $isValid; +} \ No newline at end of file diff --git a/framework/widgets/ActiveForm.php b/framework/widgets/ActiveForm.php new file mode 100644 index 0000000..2c965e7 --- /dev/null +++ b/framework/widgets/ActiveForm.php @@ -0,0 +1,278 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\widgets; + +use Yii; +use yii\base\InvalidParamException; +use yii\base\Widget; +use yii\base\Model; +use yii\helpers\Html; +use yii\helpers\ArrayHelper; + +/** + * ActiveForm ... + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class ActiveForm extends Widget +{ + /** + * @param array|string $action the form action URL. This parameter will be processed by [[\yii\helpers\Html::url()]]. + */ + public $action = ''; + /** + * @var string the form submission method. This should be either 'post' or 'get'. + * Defaults to 'post'. + */ + public $method = 'post'; + /** + * @var string the default CSS class for the error summary container. + * @see errorSummary() + */ + public $errorSummaryClass = 'yii-error-summary'; + public $errorMessageClass = 'yii-error-message'; + /** + * @var string the default CSS class that indicates an input has error. + * This is + */ + public $errorClass = 'yii-error'; + public $successClass = 'yii-success'; + public $validatingClass = 'yii-validating'; + /** + * @var boolean whether to enable client-side data validation. Defaults to false. + * When this property is set true, client-side validation will be performed by validators + * that support it (see {@link CValidator::enableClientValidation} and {@link CValidator::clientValidateAttribute}). + */ + public $enableClientValidation = false; + + public $options = array(); + /** + * @var array model-class mapped to name prefix + */ + public $modelMap; + + /** + * @param Model|Model[] $models + * @param array $options + * @return string + */ + public function errorSummary($models, $options = array()) + { + if (!is_array($models)) { + $models = array($models); + } + + $showAll = isset($options['showAll']) && $options['showAll']; + $lines = array(); + /** @var $model Model */ + foreach ($models as $model) { + if ($showAll) { + foreach ($model->getErrors() as $errors) { + $lines = array_merge($lines, $errors); + } + } else { + $lines = array_merge($lines, $model->getFirstErrors()); + } + } + + $header = isset($options['header']) ? $options['header'] : '<p>' . Yii::t('yii|Please fix the following errors:') . '</p>'; + $footer = isset($options['footer']) ? $options['footer'] : ''; + $tag = isset($options['tag']) ? $options['tag'] : 'div'; + unset($options['showAll'], $options['header'], $options['footer'], $options['container']); + + if (!isset($options['class'])) { + $options['class'] = $this->errorSummaryClass; + } else { + $options['class'] .= ' ' . $this->errorSummaryClass; + } + + if ($lines !== array()) { + $content = "<ul><li>" . implode("</li>\n<li>", ArrayHelper::htmlEncode($lines)) . "</li><ul>"; + return Html::tag($tag, $header . $content . $footer, $options); + } else { + $content = "<ul></ul>"; + $options['style'] = isset($options['style']) ? rtrim($options['style'], ';') . '; display:none' : 'display:none'; + return Html::tag($tag, $header . $content . $footer, $options); + } + } + + /** + * @param Model $model + * @param string $attribute + * @param array $options + * @return string + */ + public function error($model, $attribute, $options = array()) + { + $attribute = $this->normalizeAttributeName($attribute); + $this->getInputName($model, $attribute); + $tag = isset($options['tag']) ? $options['tag'] : 'div'; + unset($options['tag']); + $error = $model->getFirstError($attribute); + return Html::tag($tag, Html::encode($error), $options); + } + + /** + * @param Model $model + * @param string $attribute + * @param array $options + * @return string + */ + public function label($model, $attribute, $options = array()) + { + $attribute = $this->normalizeAttributeName($attribute); + $label = $model->getAttributeLabel($attribute); + return Html::label(Html::encode($label), isset($options['for']) ? $options['for'] : null, $options); + } + + public function input($type, $model, $attribute, $options = array()) + { + $value = $this->getAttributeValue($model, $attribute); + $name = $this->getInputName($model, $attribute); + return Html::input($type, $name, $value, $options); + } + + public function textInput($model, $attribute, $options = array()) + { + return $this->input('text', $model, $attribute, $options); + } + + public function hiddenInput($model, $attribute, $options = array()) + { + return $this->input('hidden', $model, $attribute, $options); + } + + public function passwordInput($model, $attribute, $options = array()) + { + return $this->input('password', $model, $attribute, $options); + } + + public function fileInput($model, $attribute, $options = array()) + { + return $this->input('file', $model, $attribute, $options); + } + + public function textarea($model, $attribute, $options = array()) + { + $value = $this->getAttributeValue($model, $attribute); + $name = $this->getInputName($model, $attribute); + return Html::textarea($name, $value, $options); + } + + public function radio($model, $attribute, $value = '1', $options = array()) + { + $checked = $this->getAttributeValue($model, $attribute); + $name = $this->getInputName($model, $attribute); + if (!array_key_exists('uncheck', $options)) { + $options['unchecked'] = '0'; + } + return Html::radio($name, $checked, $value, $options); + } + + public function checkbox($model, $attribute, $value = '1', $options = array()) + { + $checked = $this->getAttributeValue($model, $attribute); + $name = $this->getInputName($model, $attribute); + if (!array_key_exists('uncheck', $options)) { + $options['unchecked'] = '0'; + } + return Html::checkbox($name, $checked, $value, $options); + } + + public function dropDownList($model, $attribute, $items, $options = array()) + { + $checked = $this->getAttributeValue($model, $attribute); + $name = $this->getInputName($model, $attribute); + return Html::dropDownList($name, $checked, $items, $options); + } + + public function listBox($model, $attribute, $items, $options = array()) + { + $checked = $this->getAttributeValue($model, $attribute); + $name = $this->getInputName($model, $attribute); + if (!array_key_exists('unselect', $options)) { + $options['unselect'] = '0'; + } + return Html::listBox($name, $checked, $items, $options); + } + + public function checkboxList($model, $attribute, $items, $options = array()) + { + $checked = $this->getAttributeValue($model, $attribute); + $name = $this->getInputName($model, $attribute); + if (!array_key_exists('unselect', $options)) { + $options['unselect'] = '0'; + } + return Html::checkboxList($name, $checked, $items, $options); + } + + public function radioList($model, $attribute, $items, $options = array()) + { + $checked = $this->getAttributeValue($model, $attribute); + $name = $this->getInputName($model, $attribute); + if (!array_key_exists('unselect', $options)) { + $options['unselect'] = '0'; + } + return Html::radioList($name, $checked, $items, $options); + } + + public function getInputName($model, $attribute) + { + $class = get_class($model); + if (isset($this->modelMap[$class])) { + $class = $this->modelMap[$class]; + } elseif (($pos = strrpos($class, '\\')) !== false) { + $class = substr($class, $pos); + } + if (!preg_match('/(^|.*\])(\w+)(\[.*|$)/', $attribute, $matches)) { + throw new InvalidParamException('Attribute name must contain word characters only.'); + } + $prefix = $matches[1]; + $attribute = $matches[2]; + $suffix = $matches[3]; + if ($class === '' && $prefix === '') { + return $attribute . $suffix; + } elseif ($class !== '') { + return $class . $prefix . "[$attribute]" . $suffix; + } else { + throw new InvalidParamException('Model name cannot be mapped to empty for tabular inputs.'); + } + } + + public function getAttributeValue($model, $attribute) + { + if (!preg_match('/(^|.*\])(\w+)(\[.*|$)/', $attribute, $matches)) { + throw new InvalidParamException('Attribute name must contain word characters only.'); + } + $attribute = $matches[2]; + $index = $matches[3]; + if ($index === '') { + return $model->$attribute; + } else { + $value = $model->$attribute; + foreach (explode('][', trim($index, '[]')) as $id) { + if ((is_array($value) || $value instanceof \ArrayAccess) && isset($value[$id])) { + $value = $value[$id]; + } else { + return null; + } + } + return $value; + } + } + + public function normalizeAttributeName($attribute) + { + if (preg_match('/(^|.*\])(\w+)(\[.*|$)/', $attribute, $matches)) { + return $matches[2]; + } else { + throw new InvalidParamException('Attribute name must contain word characters only.'); + } + } +} diff --git a/framework/widgets/Clip.php b/framework/widgets/Clip.php new file mode 100644 index 0000000..d540b24 --- /dev/null +++ b/framework/widgets/Clip.php @@ -0,0 +1,57 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\widgets; + +use Yii; +use yii\base\Widget; +use yii\base\View; + +/** + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class Clip extends Widget +{ + /** + * @var string the ID of this clip. + */ + public $id; + /** + * @var View the view object for keeping the clip. If not set, the view registered with the application + * will be used. + */ + public $view; + /** + * @var boolean whether to render the clip content in place. Defaults to false, + * meaning the captured clip will not be displayed. + */ + public $renderInPlace = false; + + /** + * Starts recording a clip. + */ + public function init() + { + ob_start(); + ob_implicit_flush(false); + } + + /** + * Ends recording a clip. + * This method stops output buffering and saves the rendering result as a named clip in the controller. + */ + public function run() + { + $clip = ob_get_clean(); + if ($this->renderClip) { + echo $clip; + } + $view = $this->view !== null ? $this->view : Yii::$app->getView(); + $view->clips[$this->id] = $clip; + } +} \ No newline at end of file diff --git a/framework/widgets/ContentDecorator.php b/framework/widgets/ContentDecorator.php new file mode 100644 index 0000000..4c3ae70 --- /dev/null +++ b/framework/widgets/ContentDecorator.php @@ -0,0 +1,59 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\widgets; + +use Yii; +use yii\base\InvalidConfigException; +use yii\base\Widget; +use yii\base\View; + +/** + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class ContentDecorator extends Widget +{ + /** + * @var View the view object for rendering [[viewName]]. If not set, the view registered with the application + * will be used. + */ + public $view; + /** + * @var string the name of the view that will be used to decorate the content enclosed by this widget. + * Please refer to [[View::findViewFile()]] on how to set this property. + */ + public $viewName; + /** + * @var array the parameters (name=>value) to be extracted and made available in the decorative view. + */ + public $params = array(); + + /** + * Starts recording a clip. + */ + public function init() + { + if ($this->viewName === null) { + throw new InvalidConfigException('ContentDecorator::viewName must be set.'); + } + ob_start(); + ob_implicit_flush(false); + } + + /** + * Ends recording a clip. + * This method stops output buffering and saves the rendering result as a named clip in the controller. + */ + public function run() + { + $params = $this->params; + $params['content'] = ob_get_clean(); + $view = $this->view !== null ? $this->view : Yii::$app->getView(); + echo $view->render($this->viewName, $params); + } +} diff --git a/framework/widgets/FragmentCache.php b/framework/widgets/FragmentCache.php new file mode 100644 index 0000000..65bb86b --- /dev/null +++ b/framework/widgets/FragmentCache.php @@ -0,0 +1,184 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\widgets; + +use Yii; +use yii\base\InvalidConfigException; +use yii\base\Widget; +use yii\caching\Cache; +use yii\caching\Dependency; + +/** + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class FragmentCache extends Widget +{ + /** + * @var Cache|string the cache object or the application component ID of the cache object. + * After the FragmentCache object is created, if you want to change this property, + * you should only assign it with a cache object. + */ + public $cache = 'cache'; + /** + * @var integer number of seconds that the data can remain valid in cache. + * Use 0 to indicate that the cached data will never expire. + */ + public $duration = 60; + /** + * @var array|Dependency the dependency that the cached content depends on. + * This can be either a [[Dependency]] object or a configuration array for creating the dependency object. + * For example, + * + * ~~~ + * array( + * 'class' => 'yii\caching\DbDependency', + * 'sql' => 'SELECT MAX(lastModified) FROM Post', + * ) + * ~~~ + * + * would make the output cache depends on the last modified time of all posts. + * If any post has its modification time changed, the cached content would be invalidated. + */ + public $dependency; + /** + * @var array list of factors that would cause the variation of the content being cached. + * Each factor is a string representing a variation (e.g. the language, a GET parameter). + * The following variation setting will cause the content to be cached in different versions + * according to the current application language: + * + * ~~~ + * array( + * Yii::$app->language, + * ) + */ + public $variations; + /** + * @var boolean whether to enable the fragment cache. You may use this property to turn on and off + * the fragment cache according to specific setting (e.g. enable fragment cache only for GET requests). + */ + public $enabled = true; + /** + * @var \yii\base\View the view object within which this widget is used. If not set, + * the view registered with the application will be used. This is mainly used by dynamic content feature. + */ + public $view; + /** + * @var array a list of placeholders for embedding dynamic contents. This property + * is used internally to implement the content caching feature. Do not modify it. + */ + public $dynamicPlaceholders; + + /** + * Initializes the FragmentCache object. + */ + public function init() + { + parent::init(); + + if ($this->view === null) { + $this->view = Yii::$app->getView(); + } + + if (!$this->enabled) { + $this->cache = null; + } elseif (is_string($this->cache)) { + $this->cache = Yii::$app->getComponent($this->cache); + } + + if ($this->getCachedContent() === false) { + $this->view->cacheStack[] = $this; + ob_start(); + ob_implicit_flush(false); + } + } + + /** + * Marks the end of content to be cached. + * Content displayed before this method call and after {@link init()} + * will be captured and saved in cache. + * This method does nothing if valid content is already found in cache. + */ + public function run() + { + if (($content = $this->getCachedContent()) !== false) { + echo $content; + } elseif ($this->cache instanceof Cache) { + $content = ob_get_clean(); + array_pop($this->view->cacheStack); + if (is_array($this->dependency)) { + $this->dependency = Yii::createObject($this->dependency); + } + $data = array($content, $this->dynamicPlaceholders); + $this->cache->set($this->calculateKey(), $data, $this->duration, $this->dependency); + + if ($this->view->cacheStack === array() && !empty($this->dynamicPlaceholders)) { + $content = $this->updateDynamicContent($content, $this->dynamicPlaceholders); + } + echo $content; + } + } + + /** + * @var string|boolean the cached content. False if the content is not cached. + */ + private $_content; + + /** + * Returns the cached content if available. + * @return string|boolean the cached content. False is returned if valid content is not found in the cache. + */ + public function getCachedContent() + { + if ($this->_content === null) { + $this->_content = false; + if ($this->cache instanceof Cache) { + $key = $this->calculateKey(); + $data = $this->cache->get($key); + if (is_array($data) && count($data) === 2) { + list ($content, $placeholders) = $data; + if (is_array($placeholders) && count($placeholders) > 0) { + if ($this->view->cacheStack === array()) { + // outermost cache: replace placeholder with dynamic content + $content = $this->updateDynamicContent($content, $placeholders); + } + foreach ($placeholders as $name => $statements) { + $this->view->addDynamicPlaceholder($name, $statements); + } + } + $this->_content = $content; + } + } + } + return $this->_content; + } + + protected function updateDynamicContent($content, $placeholders) + { + foreach ($placeholders as $name => $statements) { + $placeholders[$name] = $this->view->evaluateDynamicContent($statements); + } + return strtr($content, $placeholders); + } + + /** + * Generates a unique key used for storing the content in cache. + * The key generated depends on both [[id]] and [[variations]]. + * @return string a valid cache key + */ + protected function calculateKey() + { + $factors = array(__CLASS__, $this->getId()); + if (is_array($this->variations)) { + foreach ($this->variations as $factor) { + $factors[] = $factor; + } + } + return $this->cache->buildKey($factors); + } +} \ No newline at end of file diff --git a/framework/yii.php b/framework/yii.php index 4e7aa5c..828dc4f 100644 --- a/framework/yii.php +++ b/framework/yii.php @@ -3,7 +3,7 @@ * Yii bootstrap file. * * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ diff --git a/framework/yiic b/framework/yiic index bea1efb..d35d262 100755 --- a/framework/yiic +++ b/framework/yiic @@ -6,7 +6,7 @@ * This is the bootstrap script for running yiic on Unix/Linux. * * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ diff --git a/framework/yiic.php b/framework/yiic.php index 55b9e60..0db69bb 100644 --- a/framework/yiic.php +++ b/framework/yiic.php @@ -3,7 +3,7 @@ * Yii console bootstrap file. * * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ diff --git a/tests/unit/.gitignore b/tests/unit/.gitignore new file mode 100644 index 0000000..34651d7 --- /dev/null +++ b/tests/unit/.gitignore @@ -0,0 +1 @@ +runtime/cache/* \ No newline at end of file diff --git a/tests/unit/bootstrap.php b/tests/unit/bootstrap.php index f60eee0..4a388c6 100644 --- a/tests/unit/bootstrap.php +++ b/tests/unit/bootstrap.php @@ -9,4 +9,4 @@ require_once(__DIR__ . '/../../framework/yii.php'); Yii::setAlias('@yiiunit', __DIR__); -require_once(__DIR__ . '/TestCase.php'); \ No newline at end of file +require_once(__DIR__ . '/TestCase.php'); diff --git a/tests/unit/data/ar/ActiveRecord.php b/tests/unit/data/ar/ActiveRecord.php index 328a597..95346de 100644 --- a/tests/unit/data/ar/ActiveRecord.php +++ b/tests/unit/data/ar/ActiveRecord.php @@ -1,9 +1,7 @@ <?php /** - * ActiveRecord class file. - * * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ diff --git a/tests/unit/data/base/InvalidRulesModel.php b/tests/unit/data/base/InvalidRulesModel.php new file mode 100644 index 0000000..f5a8438 --- /dev/null +++ b/tests/unit/data/base/InvalidRulesModel.php @@ -0,0 +1,17 @@ +<?php +namespace yiiunit\data\base; +use yii\base\Model; + +/** + * InvalidRulesModel + */ +class InvalidRulesModel extends Model +{ + public function rules() + { + return array( + array('test'), + ); + } + +} diff --git a/tests/unit/data/base/Singer.php b/tests/unit/data/base/Singer.php new file mode 100644 index 0000000..3305b98 --- /dev/null +++ b/tests/unit/data/base/Singer.php @@ -0,0 +1,21 @@ +<?php +namespace yiiunit\data\base; +use yii\base\Model; + +/** + * Singer + */ +class Singer extends Model +{ + public $fistName; + public $lastName; + + public function rules() + { + return array( + array('lastName', 'default', 'value' => 'Lennon'), + array('lastName', 'required'), + array('underscore_style', 'yii\validators\CaptchaValidator'), + ); + } +} \ No newline at end of file diff --git a/tests/unit/data/base/Speaker.php b/tests/unit/data/base/Speaker.php new file mode 100644 index 0000000..93dd496 --- /dev/null +++ b/tests/unit/data/base/Speaker.php @@ -0,0 +1,39 @@ +<?php +namespace yiiunit\data\base; +use yii\base\Model; + +/** + * Speaker + */ +class Speaker extends Model +{ + public $firstName; + public $lastName; + + public $customLabel; + public $underscore_style; + + protected $protectedProperty; + private $_privateProperty; + + public function attributeLabels() + { + return array( + 'customLabel' => 'This is the custom label', + ); + } + + public function rules() + { + return array( + + ); + } + + public function scenarios() + { + return array( + 'test' => array('firstName', 'lastName', '!underscore_style'), + ); + } +} diff --git a/tests/unit/framework/YiiBaseTest.php b/tests/unit/framework/YiiBaseTest.php new file mode 100644 index 0000000..df12bf9 --- /dev/null +++ b/tests/unit/framework/YiiBaseTest.php @@ -0,0 +1,26 @@ +<?php +namespace yiiunit\framework; + +use yiiunit\TestCase; + +/** + * YiiBaseTest + */ +class YiiBaseTest extends TestCase +{ + public function testAlias() + { + + } + + public function testGetVersion() + { + echo \Yii::getVersion(); + $this->assertTrue((boolean)preg_match('~\d+\.\d+(?:\.\d+)?(?:-\w+)?~', \Yii::getVersion())); + } + + public function testPowered() + { + $this->assertTrue(is_string(\Yii::powered())); + } +} diff --git a/tests/unit/framework/base/BehaviorTest.php b/tests/unit/framework/base/BehaviorTest.php index 4b4817b..11fbe7f 100644 --- a/tests/unit/framework/base/BehaviorTest.php +++ b/tests/unit/framework/base/BehaviorTest.php @@ -2,12 +2,16 @@ namespace yiiunit\framework\base; -class BarClass extends \yii\base\Component +use yii\base\Behavior; +use yii\base\Component; +use yiiunit\TestCase; + +class BarClass extends Component { } -class FooClass extends \yii\base\Component +class FooClass extends Component { public function behaviors() { @@ -17,7 +21,7 @@ class FooClass extends \yii\base\Component } } -class BarBehavior extends \yii\base\Behavior +class BarBehavior extends Behavior { public $behaviorProperty = 'behavior property'; @@ -27,7 +31,7 @@ class BarBehavior extends \yii\base\Behavior } } -class BehaviorTest extends \yiiunit\TestCase +class BehaviorTest extends TestCase { public function testAttachAndAccessing() { @@ -38,6 +42,10 @@ class BehaviorTest extends \yiiunit\TestCase $this->assertEquals('behavior method', $bar->behaviorMethod()); $this->assertEquals('behavior property', $bar->getBehavior('bar')->behaviorProperty); $this->assertEquals('behavior method', $bar->getBehavior('bar')->behaviorMethod()); + + $behavior = new BarBehavior(array('behaviorProperty' => 'reattached')); + $bar->attachBehavior('bar', $behavior); + $this->assertEquals('reattached', $bar->behaviorProperty); } public function testAutomaticAttach() diff --git a/tests/unit/framework/base/ComponentTest.php b/tests/unit/framework/base/ComponentTest.php index 3a4bab2..97b0116 100644 --- a/tests/unit/framework/base/ComponentTest.php +++ b/tests/unit/framework/base/ComponentTest.php @@ -1,7 +1,11 @@ <?php - namespace yiiunit\framework\base; +use yii\base\Behavior; +use yii\base\Component; +use yii\base\Event; +use yiiunit\TestCase; + function globalEventHandler($event) { $event->sender->eventHandled = true; @@ -13,7 +17,7 @@ function globalEventHandler2($event) $event->handled = true; } -class ComponentTest extends \yiiunit\TestCase +class ComponentTest extends TestCase { /** * @var NewComponent @@ -29,6 +33,21 @@ class ComponentTest extends \yiiunit\TestCase { $this->component = null; } + + public function testClone() + { + $component = new NewComponent(); + $behavior = new NewBehavior(); + $component->attachBehavior('a', $behavior); + $this->assertSame($behavior, $component->getBehavior('a')); + $component->on('test', 'fake'); + $this->assertEquals(1, $component->getEventHandlers('test')->count); + + $clone = clone $component; + $this->assertNotSame($component, $clone); + $this->assertNull($clone->getBehavior('a')); + $this->assertEquals(0, $clone->getEventHandlers('test')->count); + } public function testHasProperty() { @@ -59,6 +78,13 @@ class ComponentTest extends \yiiunit\TestCase $this->assertTrue($this->component->canSetProperty('content')); $this->assertFalse($this->component->canSetProperty('content', false)); $this->assertFalse($this->component->canSetProperty('Content')); + + // behavior + $this->assertFalse($this->component->canSetProperty('p2')); + $behavior = new NewBehavior(); + $this->component->attachBehavior('a', $behavior); + $this->assertTrue($this->component->canSetProperty('p2')); + $this->component->detachBehavior('a'); } public function testGetProperty() @@ -89,6 +115,18 @@ class ComponentTest extends \yiiunit\TestCase $this->component->Text = null; $this->assertFalse(isset($this->component->Text)); $this->assertTrue(empty($this->component->Text)); + + + $this->assertFalse(isset($this->component->p2)); + $this->component->attachBehavior('a', new NewBehavior()); + $this->component->setP2('test'); + $this->assertTrue(isset($this->component->p2)); + } + + public function testCallUnknownMethod() + { + $this->setExpectedException('yii\base\UnknownMethodException'); + $this->component->unknownMethod(); } public function testUnset() @@ -96,6 +134,19 @@ class ComponentTest extends \yiiunit\TestCase unset($this->component->Text); $this->assertFalse(isset($this->component->Text)); $this->assertTrue(empty($this->component->Text)); + + $this->component->attachBehavior('a', new NewBehavior()); + $this->component->setP2('test'); + $this->assertEquals('test', $this->component->getP2()); + + unset($this->component->p2); + $this->assertNull($this->component->getP2()); + } + + public function testUnsetReadonly() + { + $this->setExpectedException('yii\base\InvalidCallException'); + unset($this->component->object); } public function testOn() @@ -147,6 +198,14 @@ class ComponentTest extends \yiiunit\TestCase }); $this->component->raiseEvent(); $this->assertTrue($eventRaised); + + // raise event w/o parameters + $eventRaised = false; + $this->component->on('test', function($event) use (&$eventRaised) { + $eventRaised = true; + }); + $this->component->trigger('test'); + $this->assertTrue($eventRaised); } public function testHasEventHandlers() @@ -193,9 +252,57 @@ class ComponentTest extends \yiiunit\TestCase $component->test(); $this->assertTrue($component->behaviorCalled); } + + public function testAttachBehaviors() + { + $component = new NewComponent; + $this->assertNull($component->getBehavior('a')); + $this->assertNull($component->getBehavior('b')); + + $behavior = new NewBehavior; + + $component->attachBehaviors(array( + 'a' => $behavior, + 'b' => $behavior, + )); + + $this->assertSame(array('a' => $behavior, 'b' => $behavior), $component->getBehaviors()); + } + + public function testDetachBehavior() + { + $component = new NewComponent; + $behavior = new NewBehavior; + + $component->attachBehavior('a', $behavior); + $this->assertSame($behavior, $component->getBehavior('a')); + + $detachedBehavior = $component->detachBehavior('a'); + $this->assertSame($detachedBehavior, $behavior); + $this->assertNull($component->getBehavior('a')); + + $detachedBehavior = $component->detachBehavior('z'); + $this->assertNull($detachedBehavior); + } + + public function testDetachBehaviors() + { + $component = new NewComponent; + $behavior = new NewBehavior; + + $component->attachBehavior('a', $behavior); + $this->assertSame($behavior, $component->getBehavior('a')); + $component->attachBehavior('b', $behavior); + $this->assertSame($behavior, $component->getBehavior('b')); + + $component->detachBehaviors(); + $this->assertNull($component->getBehavior('a')); + $this->assertNull($component->getBehavior('b')); + + } } -class NewComponent extends \yii\base\Component +class NewComponent extends Component { private $_object = null; private $_text = 'default'; @@ -245,13 +352,24 @@ class NewComponent extends \yii\base\Component public function raiseEvent() { - $this->trigger('click', new \yii\base\Event($this)); + $this->trigger('click', new Event); } } -class NewBehavior extends \yii\base\Behavior +class NewBehavior extends Behavior { public $p; + private $p2; + + public function getP2() + { + return $this->p2; + } + + public function setP2($value) + { + $this->p2 = $value; + } public function test() { @@ -260,7 +378,7 @@ class NewBehavior extends \yii\base\Behavior } } -class NewComponent2 extends \yii\base\Component +class NewComponent2 extends Component { public $a; public $b; diff --git a/tests/unit/framework/base/DictionaryTest.php b/tests/unit/framework/base/DictionaryTest.php index 7828300..9e55547 100644 --- a/tests/unit/framework/base/DictionaryTest.php +++ b/tests/unit/framework/base/DictionaryTest.php @@ -61,29 +61,37 @@ 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'; } public function testRemove() { $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->clear(); + $this->dictionary->add('key3',$this->item3); + $this->dictionary->removeAll(); + $this->assertEquals(0,$this->dictionary->getCount()); + $this->assertTrue(!$this->dictionary->has('key1') && !$this->dictionary->has('key2')); + + $this->dictionary->add('key3',$this->item3); + $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() @@ -95,7 +103,7 @@ class DictionaryTest extends \yiiunit\TestCase $this->assertEquals($this->item3, $this->dictionary['key3']); $this->assertEquals($this->item1, $this->dictionary['key4']); - $this->setExpectedException('yii\base\InvalidCallException'); + $this->setExpectedException('yii\base\InvalidParamException'); $this->dictionary->copyFrom($this); } @@ -114,7 +122,7 @@ class DictionaryTest extends \yiiunit\TestCase $this->assertEquals(3,$this->dictionary->getCount()); $this->assertEquals($this->item1,$this->dictionary['key2']); $this->assertEquals($this->item3,$this->dictionary['key3']); - $this->setExpectedException('yii\base\InvalidCallException'); + $this->setExpectedException('yii\base\InvalidParamException'); $this->dictionary->mergeWith($this,false); } @@ -154,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/ModelTest.php b/tests/unit/framework/base/ModelTest.php new file mode 100644 index 0000000..aa15230 --- /dev/null +++ b/tests/unit/framework/base/ModelTest.php @@ -0,0 +1,203 @@ +<?php + +namespace yiiunit\framework\base; +use yii\base\Model; +use yiiunit\TestCase; +use yiiunit\data\base\Speaker; +use yiiunit\data\base\Singer; +use yiiunit\data\base\InvalidRulesModel; + +/** + * ModelTest + */ +class ModelTest extends TestCase +{ + public function testGetAttributeLalel() + { + $speaker = new Speaker(); + $this->assertEquals('First Name', $speaker->getAttributeLabel('firstName')); + $this->assertEquals('This is the custom label', $speaker->getAttributeLabel('customLabel')); + $this->assertEquals('Underscore Style', $speaker->getAttributeLabel('underscore_style')); + } + + public function testGetAttributes() + { + $speaker = new Speaker(); + $speaker->firstName = 'Qiang'; + $speaker->lastName = 'Xue'; + + $this->assertEquals(array( + 'firstName' => 'Qiang', + 'lastName' => 'Xue', + 'customLabel' => null, + 'underscore_style' => null, + ), $speaker->getAttributes()); + + $this->assertEquals(array( + 'firstName' => 'Qiang', + 'lastName' => 'Xue', + ), $speaker->getAttributes(array('firstName', 'lastName'))); + + $this->assertEquals(array( + 'firstName' => 'Qiang', + 'lastName' => 'Xue', + ), $speaker->getAttributes(null, array('customLabel', 'underscore_style'))); + + $this->assertEquals(array( + 'firstName' => 'Qiang', + ), $speaker->getAttributes(array('firstName', 'lastName'), array('lastName', 'customLabel', 'underscore_style'))); + } + + public function testSetAttributes() + { + // by default mass assignment doesn't work at all + $speaker = new Speaker(); + $speaker->setAttributes(array('firstName' => 'Qiang', 'underscore_style' => 'test')); + $this->assertNull($speaker->firstName); + $this->assertNull($speaker->underscore_style); + + // in the test scenario + $speaker = new Speaker(); + $speaker->setScenario('test'); + $speaker->setAttributes(array('firstName' => 'Qiang', 'underscore_style' => 'test')); + $this->assertNull($speaker->underscore_style); + $this->assertEquals('Qiang', $speaker->firstName); + + $speaker->setAttributes(array('firstName' => 'Qiang', 'underscore_style' => 'test'), false); + $this->assertEquals('test', $speaker->underscore_style); + $this->assertEquals('Qiang', $speaker->firstName); + } + + public function testActiveAttributes() + { + // by default mass assignment doesn't work at all + $speaker = new Speaker(); + $this->assertEmpty($speaker->activeAttributes()); + + $speaker = new Speaker(); + $speaker->setScenario('test'); + $this->assertEquals(array('firstName', 'lastName', 'underscore_style'), $speaker->activeAttributes()); + } + + public function testIsAttributeSafe() + { + // by default mass assignment doesn't work at all + $speaker = new Speaker(); + $this->assertFalse($speaker->isAttributeSafe('firstName')); + + $speaker = new Speaker(); + $speaker->setScenario('test'); + $this->assertTrue($speaker->isAttributeSafe('firstName')); + + } + + public function testErrors() + { + $speaker = new Speaker(); + + $this->assertEmpty($speaker->getErrors()); + $this->assertEmpty($speaker->getErrors('firstName')); + $this->assertEmpty($speaker->getFirstErrors()); + + $this->assertFalse($speaker->hasErrors()); + $this->assertFalse($speaker->hasErrors('firstName')); + + $speaker->addError('firstName', 'Something is wrong!'); + $this->assertEquals(array('firstName' => array('Something is wrong!')), $speaker->getErrors()); + $this->assertEquals(array('Something is wrong!'), $speaker->getErrors('firstName')); + + $speaker->addError('firstName', 'Totally wrong!'); + $this->assertEquals(array('firstName' => array('Something is wrong!', 'Totally wrong!')), $speaker->getErrors()); + $this->assertEquals(array('Something is wrong!', 'Totally wrong!'), $speaker->getErrors('firstName')); + + $this->assertTrue($speaker->hasErrors()); + $this->assertTrue($speaker->hasErrors('firstName')); + $this->assertFalse($speaker->hasErrors('lastName')); + + $this->assertEquals(array('Something is wrong!'), $speaker->getFirstErrors()); + $this->assertEquals('Something is wrong!', $speaker->getFirstError('firstName')); + $this->assertNull($speaker->getFirstError('lastName')); + + $speaker->addError('lastName', 'Another one!'); + $this->assertEquals(array( + 'firstName' => array( + 'Something is wrong!', + 'Totally wrong!', + ), + 'lastName' => array('Another one!'), + ), $speaker->getErrors()); + + $speaker->clearErrors('firstName'); + $this->assertEquals(array( + 'lastName' => array('Another one!'), + ), $speaker->getErrors()); + + $speaker->clearErrors(); + $this->assertEmpty($speaker->getErrors()); + $this->assertFalse($speaker->hasErrors()); + } + + public function testArraySyntax() + { + $speaker = new Speaker(); + + // get + $this->assertNull($speaker['firstName']); + + // isset + $this->assertFalse(isset($speaker['firstName'])); + + // set + $speaker['firstName'] = 'Qiang'; + + $this->assertEquals('Qiang', $speaker['firstName']); + $this->assertTrue(isset($speaker['firstName'])); + + // iteration + $attributes = array(); + foreach($speaker as $key => $attribute) { + $attributes[$key] = $attribute; + } + $this->assertEquals(array( + 'firstName' => 'Qiang', + 'lastName' => null, + 'customLabel' => null, + 'underscore_style' => null, + ), $attributes); + + // unset + unset($speaker['firstName']); + + // exception isn't expected here + $this->assertNull($speaker['firstName']); + $this->assertFalse(isset($speaker['firstName'])); + } + + public function testDefaults() + { + $singer = new Model(); + $this->assertEquals(array(), $singer->rules()); + $this->assertEquals(array(), $singer->attributeLabels()); + } + + public function testDefaultScenarios() + { + $singer = new Singer(); + $this->assertEquals(array('default' => array('lastName', 'underscore_style')), $singer->scenarios()); + } + + public function testIsAttributeRequired() + { + $singer = new Singer(); + $this->assertFalse($singer->isAttributeRequired('firstName')); + $this->assertTrue($singer->isAttributeRequired('lastName')); + } + + public function testCreateValidators() + { + $this->setExpectedException('yii\base\InvalidConfigException', 'Invalid validation rule: a rule must be an array specifying both attribute names and validator type.'); + + $invalid = new InvalidRulesModel(); + $invalid->createValidators(); + } +} diff --git a/tests/unit/framework/base/ObjectTest.php b/tests/unit/framework/base/ObjectTest.php index f93b4af..b47b178 100644 --- a/tests/unit/framework/base/ObjectTest.php +++ b/tests/unit/framework/base/ObjectTest.php @@ -1,11 +1,13 @@ <?php - namespace yiiunit\framework\base; +use yii\base\Object; +use yiiunit\TestCase; + /** * ObjectTest */ -class ObjectTest extends \yiiunit\TestCase +class ObjectTest extends TestCase { /** * @var NewObject @@ -69,6 +71,12 @@ class ObjectTest extends \yiiunit\TestCase $this->object->NewMember = $value; } + public function testSetReadOnlyProperty() + { + $this->setExpectedException('yii\base\InvalidCallException'); + $this->object->object = 'test'; + } + public function testIsset() { $this->assertTrue(isset($this->object->Text)); @@ -81,6 +89,9 @@ class ObjectTest extends \yiiunit\TestCase $this->object->Text = null; $this->assertFalse(isset($this->object->Text)); $this->assertTrue(empty($this->object->Text)); + + $this->assertFalse(isset($this->object->unknownProperty)); + $this->assertTrue(empty($this->object->unknownProperty)); } public function testUnset() @@ -90,6 +101,18 @@ class ObjectTest extends \yiiunit\TestCase $this->assertTrue(empty($this->object->Text)); } + public function testUnsetReadOnlyProperty() + { + $this->setExpectedException('yii\base\InvalidCallException'); + unset($this->object->object); + } + + public function testCallUnknownMethod() + { + $this->setExpectedException('yii\base\UnknownMethodException'); + $this->object->unknownMethod(); + } + public function testArrayProperty() { $this->assertEquals(array(), $this->object->items); @@ -112,10 +135,16 @@ class ObjectTest extends \yiiunit\TestCase { $this->assertEquals(2, $this->object->execute(1)); } + + public function testConstruct() + { + $object = new NewObject(array('text' => 'test text')); + $this->assertEquals('test text', $object->getText()); + } } -class NewObject extends \yii\base\Component +class NewObject extends Object { private $_object = null; private $_text = 'default'; diff --git a/tests/unit/framework/base/VectorTest.php b/tests/unit/framework/base/VectorTest.php index f7fadfd..5c44d17 100644 --- a/tests/unit/framework/base/VectorTest.php +++ b/tests/unit/framework/base/VectorTest.php @@ -44,6 +44,16 @@ class VectorTest extends \yiiunit\TestCase $this->assertEquals(2,$vector2->getCount()); } + public function testItemAt() + { + $a=array(1, 2, null, 4); + $vector=new Vector($a); + $this->assertEquals(1, $vector->itemAt(0)); + $this->assertEquals(2, $vector->itemAt(1)); + $this->assertNull($vector->itemAt(2)); + $this->assertEquals(4, $vector->itemAt(3)); + } + public function testGetCount() { $this->assertEquals(2,$this->vector->getCount()); @@ -65,7 +75,7 @@ class VectorTest extends \yiiunit\TestCase $this->assertEquals(2,$this->vector->indexOf($this->item2)); $this->assertEquals(0,$this->vector->indexOf($this->item3)); $this->assertEquals(1,$this->vector->indexOf($this->item1)); - $this->setExpectedException('yii\base\InvalidCallException'); + $this->setExpectedException('yii\base\InvalidParamException'); $this->vector->insertAt(4,$this->item3); } @@ -87,23 +97,30 @@ class VectorTest extends \yiiunit\TestCase $this->assertEquals(-1,$this->vector->indexOf($this->item2)); $this->assertEquals(1,$this->vector->indexOf($this->item3)); $this->assertEquals(0,$this->vector->indexOf($this->item1)); - $this->setExpectedException('yii\base\InvalidCallException'); + $this->setExpectedException('yii\base\InvalidParamException'); $this->vector->removeAt(2); } - public function testClear() + public function testRemoveAll() { - $this->vector->clear(); + $this->vector->add($this->item3); + $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->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() @@ -118,7 +135,7 @@ class VectorTest extends \yiiunit\TestCase $array=array($this->item3,$this->item1); $this->vector->copyFrom($array); $this->assertTrue(count($array)==2 && $this->vector[0]===$this->item3 && $this->vector[1]===$this->item1); - $this->setExpectedException('yii\base\InvalidCallException'); + $this->setExpectedException('yii\base\InvalidParamException'); $this->vector->copyFrom($this); } @@ -127,7 +144,13 @@ class VectorTest extends \yiiunit\TestCase $array=array($this->item3,$this->item1); $this->vector->mergeWith($array); $this->assertTrue($this->vector->getCount()==4 && $this->vector[0]===$this->item1 && $this->vector[3]===$this->item1); - $this->setExpectedException('yii\base\InvalidCallException'); + + $a=array(1); + $vector=new Vector($a); + $this->vector->mergeWith($vector); + $this->assertTrue($this->vector->getCount()==5 && $this->vector[0]===$this->item1 && $this->vector[3]===$this->item1 && $this->vector[4]===1); + + $this->setExpectedException('yii\base\InvalidParamException'); $this->vector->mergeWith($this); } @@ -141,7 +164,7 @@ class VectorTest extends \yiiunit\TestCase { $this->assertTrue($this->vector[0]===$this->item1); $this->assertTrue($this->vector[1]===$this->item2); - $this->setExpectedException('yii\base\InvalidCallException'); + $this->setExpectedException('yii\base\InvalidParamException'); $a=$this->vector[2]; } diff --git a/tests/unit/framework/caching/ApcCacheTest.php b/tests/unit/framework/caching/ApcCacheTest.php new file mode 100644 index 0000000..74ede2a --- /dev/null +++ b/tests/unit/framework/caching/ApcCacheTest.php @@ -0,0 +1,27 @@ +<?php +namespace yiiunit\framework\caching; +use yii\caching\ApcCache; +use yiiunit\TestCase; + +/** + * Class for testing APC cache backend + */ +class ApcCacheTest extends CacheTest +{ + private $_cacheInstance = null; + + /** + * @return ApcCache + */ + protected function getCacheInstance() + { + if(!extension_loaded("apc")) { + $this->markTestSkipped("APC not installed. Skipping."); + } + + if($this->_cacheInstance === null) { + $this->_cacheInstance = new ApcCache(); + } + return $this->_cacheInstance; + } +} \ No newline at end of file diff --git a/tests/unit/framework/caching/CacheTest.php b/tests/unit/framework/caching/CacheTest.php new file mode 100644 index 0000000..ad2fcf5 --- /dev/null +++ b/tests/unit/framework/caching/CacheTest.php @@ -0,0 +1,82 @@ +<?php +namespace yiiunit\framework\caching; +use yiiunit\TestCase; +use yii\caching\Cache; + +/** + * Base class for testing cache backends + */ +abstract class CacheTest extends TestCase +{ + /** + * @return Cache + */ + abstract protected function getCacheInstance(); + + public function testSet() + { + $cache = $this->getCacheInstance(); + $cache->set('string_test', 'string_test'); + $cache->set('number_test', 42); + $cache->set('array_test', array('array_test' => 'array_test')); + $cache['arrayaccess_test'] = new \stdClass(); + } + + public function testGet() + { + $cache = $this->getCacheInstance(); + $this->assertEquals('string_test', $cache->get('string_test')); + + $this->assertEquals(42, $cache->get('number_test')); + + $array = $cache->get('array_test'); + $this->assertArrayHasKey('array_test', $array); + $this->assertEquals('array_test', $array['array_test']); + + $this->assertInstanceOf('stdClass', $cache['arrayaccess_test']); + } + + public function testMget() + { + $cache = $this->getCacheInstance(); + $this->assertEquals(array('string_test' => 'string_test', 'number_test' => 42), $cache->mget(array('string_test', 'number_test'))); + } + + public function testExpire() + { + $cache = $this->getCacheInstance(); + $cache->set('expire_test', 'expire_test', 2); + sleep(1); + $this->assertEquals('expire_test', $cache->get('expire_test')); + sleep(2); + $this->assertEquals(false, $cache->get('expire_test')); + } + + public function testAdd() + { + $cache = $this->getCacheInstance(); + + // should not change existing keys + $cache->add('number_test', 13); + $this->assertEquals(42, $cache->get('number_test')); + + // should store data is it's not there yet + $cache->add('add_test', 13); + $this->assertEquals(13, $cache->get('add_test')); + } + + public function testDelete() + { + $cache = $this->getCacheInstance(); + + $cache->delete('number_test'); + $this->assertEquals(null, $cache->get('number_test')); + } + + public function testFlush() + { + $cache = $this->getCacheInstance(); + $cache->flush(); + $this->assertEquals(null, $cache->get('add_test')); + } +} diff --git a/tests/unit/framework/caching/DbCacheTest.php b/tests/unit/framework/caching/DbCacheTest.php new file mode 100644 index 0000000..3977ee8 --- /dev/null +++ b/tests/unit/framework/caching/DbCacheTest.php @@ -0,0 +1,70 @@ +<?php +namespace yiiunit\framework\caching; +use yii\caching\DbCache; +use yiiunit\TestCase; + +/** + * Class for testing file cache backend + */ +class DbCacheTest extends CacheTest +{ + private $_cacheInstance; + private $_connection; + + function __construct() + { + if (!extension_loaded('pdo') || !extension_loaded('pdo_mysql')) { + $this->markTestSkipped('pdo and pdo_mysql extensions are required.'); + } + + $this->getConnection()->createCommand(" + CREATE TABLE IF NOT EXISTS tbl_cache ( + id char(128) NOT NULL, + expire int(11) DEFAULT NULL, + data LONGBLOB, + PRIMARY KEY (id), + KEY expire (expire) + ); + ")->execute(); + } + + /** + * @param bool $reset whether to clean up the test database + * @return \yii\db\Connection + */ + function getConnection($reset = true) + { + if($this->_connection === null) { + $params = $this->getParam('mysql'); + $db = new \yii\db\Connection; + $db->dsn = $params['dsn']; + $db->username = $params['username']; + $db->password = $params['password']; + if ($reset) { + $db->open(); + $lines = explode(';', file_get_contents($params['fixture'])); + foreach ($lines as $line) { + if (trim($line) !== '') { + $db->pdo->exec($line); + } + } + } + $this->_connection = $db; + } + return $this->_connection; + } + + + /** + * @return DbCache + */ + protected function getCacheInstance() + { + if($this->_cacheInstance === null) { + $this->_cacheInstance = new DbCache(array( + 'db' => $this->getConnection(), + )); + } + return $this->_cacheInstance; + } +} \ No newline at end of file diff --git a/tests/unit/framework/caching/FileCacheTest.php b/tests/unit/framework/caching/FileCacheTest.php new file mode 100644 index 0000000..1f6debd --- /dev/null +++ b/tests/unit/framework/caching/FileCacheTest.php @@ -0,0 +1,25 @@ +<?php +namespace yiiunit\framework\caching; +use yii\caching\FileCache; +use yiiunit\TestCase; + +/** + * Class for testing file cache backend + */ +class FileCacheTest extends CacheTest +{ + private $_cacheInstance = null; + + /** + * @return FileCache + */ + protected function getCacheInstance() + { + if($this->_cacheInstance === null) { + $this->_cacheInstance = new FileCache(array( + 'cachePath' => '@yiiunit/runtime/cache', + )); + } + return $this->_cacheInstance; + } +} \ No newline at end of file diff --git a/tests/unit/framework/caching/MemCacheTest.php b/tests/unit/framework/caching/MemCacheTest.php new file mode 100644 index 0000000..e4804d9 --- /dev/null +++ b/tests/unit/framework/caching/MemCacheTest.php @@ -0,0 +1,27 @@ +<?php +namespace yiiunit\framework\caching; +use yii\caching\MemCache; +use yiiunit\TestCase; + +/** + * Class for testing memcache cache backend + */ +class MemCacheTest extends CacheTest +{ + private $_cacheInstance = null; + + /** + * @return MemCache + */ + protected function getCacheInstance() + { + if(!extension_loaded("memcache")) { + $this->markTestSkipped("memcache not installed. Skipping."); + } + + if($this->_cacheInstance === null) { + $this->_cacheInstance = new MemCache(); + } + return $this->_cacheInstance; + } +} \ No newline at end of file diff --git a/tests/unit/framework/caching/MemCachedTest.php b/tests/unit/framework/caching/MemCachedTest.php new file mode 100644 index 0000000..59396df --- /dev/null +++ b/tests/unit/framework/caching/MemCachedTest.php @@ -0,0 +1,29 @@ +<?php +namespace yiiunit\framework\caching; +use yii\caching\MemCache; +use yiiunit\TestCase; + +/** + * Class for testing memcache cache backend + */ +class MemCachedTest extends CacheTest +{ + private $_cacheInstance = null; + + /** + * @return MemCache + */ + protected function getCacheInstance() + { + if(!extension_loaded("memcached")) { + $this->markTestSkipped("memcached not installed. Skipping."); + } + + if($this->_cacheInstance === null) { + $this->_cacheInstance = new MemCache(array( + 'useMemcached' => true, + )); + } + return $this->_cacheInstance; + } +} \ No newline at end of file diff --git a/tests/unit/framework/caching/WinCacheTest.php b/tests/unit/framework/caching/WinCacheTest.php new file mode 100644 index 0000000..b78d57b --- /dev/null +++ b/tests/unit/framework/caching/WinCacheTest.php @@ -0,0 +1,31 @@ +<?php +namespace yiiunit\framework\caching; +use yii\caching\FileCache; +use yiiunit\TestCase; + +/** + * Class for testing wincache backend + */ +class WinCacheTest extends CacheTest +{ + private $_cacheInstance = null; + + /** + * @return WinCache + */ + protected function getCacheInstance() + { + if(!extension_loaded('wincache')) { + $this->markTestSkipped("Wincache not installed. Skipping."); + } + + if(!ini_get('wincache.ucenabled')) { + $this->markTestSkipped("Wincache user cache disabled. Skipping."); + } + + if($this->_cacheInstance === null) { + $this->_cacheInstance = new WinCache(); + } + return $this->_cacheInstance; + } +} \ No newline at end of file diff --git a/tests/unit/framework/caching/XCacheTest.php b/tests/unit/framework/caching/XCacheTest.php new file mode 100644 index 0000000..e1ed844 --- /dev/null +++ b/tests/unit/framework/caching/XCacheTest.php @@ -0,0 +1,27 @@ +<?php +namespace yiiunit\framework\caching; +use yii\caching\XCache; +use yiiunit\TestCase; + +/** + * Class for testing xcache backend + */ +class XCacheTest extends CacheTest +{ + private $_cacheInstance = null; + + /** + * @return XCache + */ + protected function getCacheInstance() + { + if(!function_exists("xcache_isset")) { + $this->markTestSkipped("XCache not installed. Skipping."); + } + + if($this->_cacheInstance === null) { + $this->_cacheInstance = new XCache(); + } + return $this->_cacheInstance; + } +} \ No newline at end of file diff --git a/tests/unit/framework/caching/ZendDataCacheTest.php b/tests/unit/framework/caching/ZendDataCacheTest.php new file mode 100644 index 0000000..91dfbb5 --- /dev/null +++ b/tests/unit/framework/caching/ZendDataCacheTest.php @@ -0,0 +1,27 @@ +<?php +namespace yiiunit\framework\caching; +use yii\caching\ZendDataCache; +use yiiunit\TestCase; + +/** + * Class for testing Zend cache backend + */ +class ZendDataCacheTest extends CacheTest +{ + private $_cacheInstance = null; + + /** + * @return ZendDataCache + */ + protected function getCacheInstance() + { + if(!function_exists("zend_shm_cache_store")) { + $this->markTestSkipped("Zend Data cache not installed. Skipping."); + } + + if($this->_cacheInstance === null) { + $this->_cacheInstance = new ZendDataCache(); + } + return $this->_cacheInstance; + } +} \ No newline at end of file diff --git a/tests/unit/framework/db/CommandTest.php b/tests/unit/framework/db/CommandTest.php index 2a11fcb..d505f6d 100644 --- a/tests/unit/framework/db/CommandTest.php +++ b/tests/unit/framework/db/CommandTest.php @@ -189,14 +189,6 @@ class CommandTest extends \yiiunit\MysqlTestCase $command = $db->createCommand($sql); $command->bindValue(':name', 'user5'); $this->assertEquals('user5@example.com', $command->queryScalar()); - - // bind value via query or execute method - $sql = 'INSERT INTO tbl_customer(email, name, address) VALUES (:email, \'user6\', \'address6\')'; - $command = $db->createCommand($sql); - $command->execute(array(':email' => 'user6@example.com')); - $sql = 'SELECT email FROM tbl_customer WHERE name=:name'; - $command = $db->createCommand($sql); - $this->assertEquals('user5@example.com', $command->queryScalar(array(':name' => 'user5'))); } function testFetchMode() diff --git a/tests/unit/framework/db/QueryTest.php b/tests/unit/framework/db/QueryTest.php index 645b844..2c4359f 100644 --- a/tests/unit/framework/db/QueryTest.php +++ b/tests/unit/framework/db/QueryTest.php @@ -14,13 +14,13 @@ class QueryTest extends \yiiunit\MysqlTestCase // default $query = new Query; $query->select('*'); - $this->assertEquals('*', $query->select); + $this->assertEquals(array('*'), $query->select); $this->assertNull($query->distinct); $this->assertEquals(null, $query->selectOption); $query = new Query; $query->select('id, name', 'something')->distinct(true); - $this->assertEquals('id, name', $query->select); + $this->assertEquals(array('id','name'), $query->select); $this->assertTrue($query->distinct); $this->assertEquals('something', $query->selectOption); } @@ -29,7 +29,7 @@ class QueryTest extends \yiiunit\MysqlTestCase { $query = new Query; $query->from('tbl_user'); - $this->assertEquals('tbl_user', $query->from); + $this->assertEquals(array('tbl_user'), $query->from); } function testWhere() @@ -57,12 +57,12 @@ class QueryTest extends \yiiunit\MysqlTestCase { $query = new Query; $query->groupBy('team'); - $this->assertEquals('team', $query->groupBy); + $this->assertEquals(array('team'), $query->groupBy); - $query->addGroup('company'); + $query->addGroupBy('company'); $this->assertEquals(array('team', 'company'), $query->groupBy); - $query->addGroup('age'); + $query->addGroupBy('age'); $this->assertEquals(array('team', 'company', 'age'), $query->groupBy); } @@ -86,13 +86,19 @@ class QueryTest extends \yiiunit\MysqlTestCase { $query = new Query; $query->orderBy('team'); - $this->assertEquals('team', $query->orderBy); + $this->assertEquals(array('team' => false), $query->orderBy); $query->addOrderBy('company'); - $this->assertEquals(array('team', 'company'), $query->orderBy); + $this->assertEquals(array('team' => false, 'company' => false), $query->orderBy); $query->addOrderBy('age'); - $this->assertEquals(array('team', 'company', 'age'), $query->orderBy); + $this->assertEquals(array('team' => false, 'company' => false, 'age' => false), $query->orderBy); + + $query->addOrderBy(array('age' => true)); + $this->assertEquals(array('team' => false, 'company' => false, 'age' => true), $query->orderBy); + + $query->addOrderBy('age ASC, company DESC'); + $this->assertEquals(array('team' => false, 'company' => true, 'age' => false), $query->orderBy); } function testLimitOffset() diff --git a/tests/unit/framework/util/ArrayHelperTest.php b/tests/unit/framework/util/ArrayHelperTest.php index a713381..117c702 100644 --- a/tests/unit/framework/util/ArrayHelperTest.php +++ b/tests/unit/framework/util/ArrayHelperTest.php @@ -1,8 +1,8 @@ <?php -namespace yiiunit\framework\db; +namespace yiiunit\framework\util; -use yii\util\ArrayHelper; +use yii\helpers\ArrayHelper; class ArrayHelperTest extends \yii\test\TestCase { diff --git a/tests/unit/framework/util/HtmlTest.php b/tests/unit/framework/util/HtmlTest.php new file mode 100644 index 0000000..eba1a20 --- /dev/null +++ b/tests/unit/framework/util/HtmlTest.php @@ -0,0 +1,448 @@ +<?php + +namespace yiiunit\framework\util; + +use Yii; +use yii\helpers\Html; +use yii\web\Application; + +class HtmlTest extends \yii\test\TestCase +{ + public function setUp() + { + new Application('test', '@yiiunit/runtime', array( + 'components' => array( + 'request' => array( + 'class' => 'yii\web\Request', + 'url' => '/test', + ), + ), + )); + } + + public function tearDown() + { + Yii::$app = null; + } + + public function testEncode() + { + $this->assertEquals("a<>&"'", Html::encode("a<>&\"'")); + } + + public function testDecode() + { + $this->assertEquals("a<>&\"'", Html::decode("a<>&"'")); + } + + public function testTag() + { + $this->assertEquals('<br />', Html::tag('br')); + $this->assertEquals('<span></span>', Html::tag('span')); + $this->assertEquals('<div>content</div>', Html::tag('div', 'content')); + $this->assertEquals('<input type="text" name="test" value="<>" />', Html::tag('input', '', array('type' => 'text', 'name' => 'test', 'value' => '<>'))); + + Html::$closeVoidElements = false; + + $this->assertEquals('<br>', Html::tag('br')); + $this->assertEquals('<span></span>', Html::tag('span')); + $this->assertEquals('<div>content</div>', Html::tag('div', 'content')); + $this->assertEquals('<input type="text" name="test" value="<>">', Html::tag('input', '', array('type' => 'text', 'name' => 'test', 'value' => '<>'))); + + Html::$closeVoidElements = true; + + $this->assertEquals('<span disabled="disabled"></span>', Html::tag('span', '', array('disabled' => true))); + Html::$showBooleanAttributeValues = false; + $this->assertEquals('<span disabled></span>', Html::tag('span', '', array('disabled' => true))); + Html::$showBooleanAttributeValues = true; + } + + public function testBeginTag() + { + $this->assertEquals('<br>', Html::beginTag('br')); + $this->assertEquals('<span id="test" class="title">', Html::beginTag('span', array('id' => 'test', 'class' => 'title'))); + } + + public function testEndTag() + { + $this->assertEquals('</br>', Html::endTag('br')); + $this->assertEquals('</span>', Html::endTag('span')); + } + + public function testCdata() + { + $data = 'test<>'; + $this->assertEquals('<![CDATA[' . $data . ']]>', Html::cdata($data)); + } + + public function testStyle() + { + $content = 'a <>'; + $this->assertEquals("<style type=\"text/css\">/*<![CDATA[*/\n{$content}\n/*]]>*/</style>", Html::style($content)); + $this->assertEquals("<style type=\"text/less\">/*<![CDATA[*/\n{$content}\n/*]]>*/</style>", Html::style($content, array('type' => 'text/less'))); + } + + public function testScript() + { + $content = 'a <>'; + $this->assertEquals("<script type=\"text/javascript\">/*<![CDATA[*/\n{$content}\n/*]]>*/</script>", Html::script($content)); + $this->assertEquals("<script type=\"text/js\">/*<![CDATA[*/\n{$content}\n/*]]>*/</script>", Html::script($content, array('type' => 'text/js'))); + } + + public function testCssFile() + { + $this->assertEquals('<link type="text/css" href="http://example.com" rel="stylesheet" />', Html::cssFile('http://example.com')); + $this->assertEquals('<link type="text/css" href="/test" rel="stylesheet" />', Html::cssFile('')); + } + + public function testJsFile() + { + $this->assertEquals('<script type="text/javascript" src="http://example.com"></script>', Html::jsFile('http://example.com')); + $this->assertEquals('<script type="text/javascript" src="/test"></script>', Html::jsFile('')); + } + + public function testBeginForm() + { + $this->assertEquals('<form action="/test" method="post">', Html::beginForm()); + $this->assertEquals('<form action="/example" method="get">', Html::beginForm('/example', 'get')); + $hiddens = array( + '<input type="hidden" name="id" value="1" />', + '<input type="hidden" name="title" value="<" />', + ); + $this->assertEquals('<form action="/example" method="get">' . "\n" . implode("\n", $hiddens), Html::beginForm('/example?id=1&title=%3C', 'get')); + } + + public function testEndForm() + { + $this->assertEquals('</form>', Html::endForm()); + } + + public function testA() + { + $this->assertEquals('<a>something<></a>', Html::a('something<>')); + $this->assertEquals('<a href="/example">something</a>', Html::a('something', '/example')); + $this->assertEquals('<a href="/test">something</a>', Html::a('something', '')); + } + + public function testMailto() + { + $this->assertEquals('<a href="mailto:test<>">test<></a>', Html::mailto('test<>')); + $this->assertEquals('<a href="mailto:test>">test<></a>', Html::mailto('test<>', 'test>')); + } + + public function testImg() + { + $this->assertEquals('<img src="/example" alt="" />', Html::img('/example')); + $this->assertEquals('<img src="/test" alt="" />', Html::img('')); + $this->assertEquals('<img src="/example" width="10" alt="something" />', Html::img('/example', array('alt' => 'something', 'width' => 10))); + } + + public function testLabel() + { + $this->assertEquals('<label>something<></label>', Html::label('something<>')); + $this->assertEquals('<label for="a">something<></label>', Html::label('something<>', 'a')); + $this->assertEquals('<label class="test" for="a">something<></label>', Html::label('something<>', 'a', array('class' => 'test'))); + } + + public function testButton() + { + $this->assertEquals('<button type="button">Button</button>', Html::button()); + $this->assertEquals('<button type="button" name="test" value="value">content<></button>', Html::button('test', 'value', 'content<>')); + $this->assertEquals('<button type="submit" class="t" name="test" value="value">content<></button>', Html::button('test', 'value', 'content<>', array('type' => 'submit', 'class' => "t"))); + } + + public function testSubmitButton() + { + $this->assertEquals('<button type="submit">Submit</button>', Html::submitButton()); + $this->assertEquals('<button type="submit" class="t" name="test" value="value">content<></button>', Html::submitButton('test', 'value', 'content<>', array('class' => 't'))); + } + + public function testResetButton() + { + $this->assertEquals('<button type="reset">Reset</button>', Html::resetButton()); + $this->assertEquals('<button type="reset" class="t" name="test" value="value">content<></button>', Html::resetButton('test', 'value', 'content<>', array('class' => 't'))); + } + + public function testInput() + { + $this->assertEquals('<input type="text" />', Html::input('text')); + $this->assertEquals('<input type="text" class="t" name="test" value="value" />', Html::input('text', 'test', 'value', array('class' => 't'))); + } + + public function testButtonInput() + { + $this->assertEquals('<input type="button" name="test" value="Button" />', Html::buttonInput('test')); + $this->assertEquals('<input type="button" class="a" name="test" value="text" />', Html::buttonInput('test', 'text', array('class' => 'a'))); + } + + public function testSubmitInput() + { + $this->assertEquals('<input type="submit" value="Submit" />', Html::submitInput()); + $this->assertEquals('<input type="submit" class="a" name="test" value="text" />', Html::submitInput('test', 'text', array('class' => 'a'))); + } + + public function testResetInput() + { + $this->assertEquals('<input type="reset" value="Reset" />', Html::resetInput()); + $this->assertEquals('<input type="reset" class="a" name="test" value="text" />', Html::resetInput('test', 'text', array('class' => 'a'))); + } + + public function testTextInput() + { + $this->assertEquals('<input type="text" name="test" />', Html::textInput('test')); + $this->assertEquals('<input type="text" class="t" name="test" value="value" />', Html::textInput('test', 'value', array('class' => 't'))); + } + + public function testHiddenInput() + { + $this->assertEquals('<input type="hidden" name="test" />', Html::hiddenInput('test')); + $this->assertEquals('<input type="hidden" class="t" name="test" value="value" />', Html::hiddenInput('test', 'value', array('class' => 't'))); + } + + public function testPasswordInput() + { + $this->assertEquals('<input type="password" name="test" />', Html::passwordInput('test')); + $this->assertEquals('<input type="password" class="t" name="test" value="value" />', Html::passwordInput('test', 'value', array('class' => 't'))); + } + + public function testFileInput() + { + $this->assertEquals('<input type="file" name="test" />', Html::fileInput('test')); + $this->assertEquals('<input type="file" class="t" name="test" value="value" />', Html::fileInput('test', 'value', array('class' => 't'))); + } + + public function testTextarea() + { + $this->assertEquals('<textarea name="test"></textarea>', Html::textarea('test')); + $this->assertEquals('<textarea class="t" name="test">value<></textarea>', Html::textarea('test', 'value<>', array('class' => 't'))); + } + + public function testRadio() + { + $this->assertEquals('<input type="radio" name="test" value="1" />', Html::radio('test')); + $this->assertEquals('<input type="radio" class="a" name="test" checked="checked" />', Html::radio('test', true, null, array('class' => 'a'))); + $this->assertEquals('<input type="hidden" name="test" value="0" /><input type="radio" class="a" name="test" value="2" checked="checked" />', Html::radio('test', true, 2, array('class' => 'a' , 'uncheck' => '0'))); + } + + public function testCheckbox() + { + $this->assertEquals('<input type="checkbox" name="test" value="1" />', Html::checkbox('test')); + $this->assertEquals('<input type="checkbox" class="a" name="test" checked="checked" />', Html::checkbox('test', true, null, array('class' => 'a'))); + $this->assertEquals('<input type="hidden" name="test" value="0" /><input type="checkbox" class="a" name="test" value="2" checked="checked" />', Html::checkbox('test', true, 2, array('class' => 'a', 'uncheck' => '0'))); + } + + public function testDropDownList() + { + $expected = <<<EOD +<select name="test"> + +</select> +EOD; + $this->assertEquals($expected, Html::dropDownList('test')); + $expected = <<<EOD +<select name="test"> +<option value="value1">text1</option> +<option value="value2">text2</option> +</select> +EOD; + $this->assertEquals($expected, Html::dropDownList('test', null, $this->getDataItems())); + $expected = <<<EOD +<select name="test"> +<option value="value1">text1</option> +<option value="value2" selected="selected">text2</option> +</select> +EOD; + $this->assertEquals($expected, Html::dropDownList('test', 'value2', $this->getDataItems())); + } + + public function testListBox() + { + $expected = <<<EOD +<select name="test" size="4"> + +</select> +EOD; + $this->assertEquals($expected, Html::listBox('test')); + $expected = <<<EOD +<select name="test" size="5"> +<option value="value1">text1</option> +<option value="value2">text2</option> +</select> +EOD; + $this->assertEquals($expected, Html::listBox('test', null, $this->getDataItems(), array('size' => 5))); + $expected = <<<EOD +<select name="test" size="4"> +<option value="value1<>">text1<></option> +<option value="value 2">text 2</option> +</select> +EOD; + $this->assertEquals($expected, Html::listBox('test', null, $this->getDataItems2())); + $expected = <<<EOD +<select name="test" size="4"> +<option value="value1">text1</option> +<option value="value2" selected="selected">text2</option> +</select> +EOD; + $this->assertEquals($expected, Html::listBox('test', 'value2', $this->getDataItems())); + $expected = <<<EOD +<select name="test" size="4"> +<option value="value1" selected="selected">text1</option> +<option value="value2" selected="selected">text2</option> +</select> +EOD; + $this->assertEquals($expected, Html::listBox('test', array('value1', 'value2'), $this->getDataItems())); + + $expected = <<<EOD +<select name="test[]" multiple="multiple" size="4"> + +</select> +EOD; + $this->assertEquals($expected, Html::listBox('test', null, array(), array('multiple' => true))); + $expected = <<<EOD +<input type="hidden" name="test" value="0" /><select name="test" size="4"> + +</select> +EOD; + $this->assertEquals($expected, Html::listBox('test', '', array(), array('unselect' => '0'))); + } + + public function testCheckboxList() + { + $this->assertEquals('', Html::checkboxList('test')); + + $expected = <<<EOD +<label><input type="checkbox" name="test[]" value="value1" /> text1</label> +<label><input type="checkbox" name="test[]" value="value2" checked="checked" /> text2</label> +EOD; + $this->assertEquals($expected, Html::checkboxList('test', array('value2'), $this->getDataItems())); + + $expected = <<<EOD +<label><input type="checkbox" name="test[]" value="value1<>" /> text1<></label> +<label><input type="checkbox" name="test[]" value="value 2" /> text 2</label> +EOD; + $this->assertEquals($expected, Html::checkboxList('test', array('value2'), $this->getDataItems2())); + + $expected = <<<EOD +<input type="hidden" name="test" value="0" /><label><input type="checkbox" name="test[]" value="value1" /> text1</label><br /> +<label><input type="checkbox" name="test[]" value="value2" checked="checked" /> text2</label> +EOD; + $this->assertEquals($expected, Html::checkboxList('test', array('value2'), $this->getDataItems(), array( + 'separator' => "<br />\n", + 'unselect' => '0', + ))); + + $expected = <<<EOD +0<label>text1 <input type="checkbox" name="test[]" value="value1" /></label> +1<label>text2 <input type="checkbox" name="test[]" value="value2" checked="checked" /></label> +EOD; + $this->assertEquals($expected, Html::checkboxList('test', array('value2'), $this->getDataItems(), array( + 'item' => function ($index, $label, $name, $checked, $value) { + return $index . Html::label($label . ' ' . Html::checkbox($name, $checked, $value)); + } + ))); + } + + public function testRadioList() + { + $this->assertEquals('', Html::radioList('test')); + + $expected = <<<EOD +<label><input type="radio" name="test" value="value1" /> text1</label> +<label><input type="radio" name="test" value="value2" checked="checked" /> text2</label> +EOD; + $this->assertEquals($expected, Html::radioList('test', array('value2'), $this->getDataItems())); + + $expected = <<<EOD +<label><input type="radio" name="test" value="value1<>" /> text1<></label> +<label><input type="radio" name="test" value="value 2" /> text 2</label> +EOD; + $this->assertEquals($expected, Html::radioList('test', array('value2'), $this->getDataItems2())); + + $expected = <<<EOD +<input type="hidden" name="test" value="0" /><label><input type="radio" name="test" value="value1" /> text1</label><br /> +<label><input type="radio" name="test" value="value2" checked="checked" /> text2</label> +EOD; + $this->assertEquals($expected, Html::radioList('test', array('value2'), $this->getDataItems(), array( + 'separator' => "<br />\n", + 'unselect' => '0', + ))); + + $expected = <<<EOD +0<label>text1 <input type="radio" name="test" value="value1" /></label> +1<label>text2 <input type="radio" name="test" value="value2" checked="checked" /></label> +EOD; + $this->assertEquals($expected, Html::radioList('test', array('value2'), $this->getDataItems(), array( + 'item' => function ($index, $label, $name, $checked, $value) { + return $index . Html::label($label . ' ' . Html::radio($name, $checked, $value)); + } + ))); + } + + public function testRenderOptions() + { + $data = array( + 'value1' => 'label1', + 'group1' => array( + 'value11' => 'label11', + 'group11' => array( + 'value111' => 'label111', + ), + 'group12' => array(), + ), + 'value2' => 'label2', + 'group2' => array(), + ); + $expected = <<<EOD +<option value="">please select<></option> +<option value="value1" selected="selected">label1</option> +<optgroup label="group1"> +<option value="value11">label11</option> +<optgroup label="group11"> +<option class="option" value="value111" selected="selected">label111</option> +</optgroup> +<optgroup class="group" label="group12"> + +</optgroup> +</optgroup> +<option value="value2">label2</option> +<optgroup label="group2"> + +</optgroup> +EOD; + $attributes = array( + 'prompt' => 'please select<>', + 'options' => array( + 'value111' => array('class' => 'option'), + ), + 'groups' => array( + 'group12' => array('class' => 'group'), + ), + ); + $this->assertEquals($expected, Html::renderSelectOptions(array('value111', 'value1'), $data, $attributes)); + } + + public function testRenderAttributes() + { + $this->assertEquals('', Html::renderTagAttributes(array())); + $this->assertEquals(' name="test" value="1<>"', Html::renderTagAttributes(array('name' => 'test', 'empty' => null, 'value' => '1<>'))); + Html::$showBooleanAttributeValues = false; + $this->assertEquals(' checked disabled', Html::renderTagAttributes(array('checked' => 'checked', 'disabled' => true, 'hidden' => false))); + Html::$showBooleanAttributeValues = true; + } + + protected function getDataItems() + { + return array( + 'value1' => 'text1', + 'value2' => 'text2', + ); + } + + protected function getDataItems2() + { + return array( + 'value1<>' => 'text1<>', + 'value 2' => 'text 2', + ); + } +} diff --git a/tests/unit/framework/validators/EmailValidatorTest.php b/tests/unit/framework/validators/EmailValidatorTest.php new file mode 100644 index 0000000..fbc2f53 --- /dev/null +++ b/tests/unit/framework/validators/EmailValidatorTest.php @@ -0,0 +1,28 @@ +<?php +namespace yiiunit\framework\validators; +use yii\validators\EmailValidator; +use yiiunit\TestCase; + +/** + * EmailValidatorTest + */ +class EmailValidatorTest extends TestCase +{ + public function testValidateValue() + { + $validator = new EmailValidator(); + + $this->assertTrue($validator->validateValue('sam@rmcreative.ru')); + $this->assertTrue($validator->validateValue('5011@gmail.com')); + $this->assertFalse($validator->validateValue('rmcreative.ru')); + } + + public function testValidateValueMx() + { + $validator = new EmailValidator(); + $validator->checkMX = true; + + $this->assertTrue($validator->validateValue('sam@rmcreative.ru')); + $this->assertFalse($validator->validateValue('test@example.com')); + } +} \ No newline at end of file diff --git a/tests/unit/framework/web/UrlManagerTest.php b/tests/unit/framework/web/UrlManagerTest.php new file mode 100644 index 0000000..95b3bf6 --- /dev/null +++ b/tests/unit/framework/web/UrlManagerTest.php @@ -0,0 +1,210 @@ +<?php +namespace yiiunit\framework\web; + +use yii\web\Request; +use yii\web\UrlManager; + +class UrlManagerTest extends \yiiunit\TestCase +{ + public function testCreateUrl() + { + // default setting with '/' as base url + $manager = new UrlManager(array( + 'baseUrl' => '/', + 'cache' => null, + )); + $url = $manager->createUrl('post/view'); + $this->assertEquals('/?r=post/view', $url); + $url = $manager->createUrl('post/view', array('id' => 1, 'title' => 'sample post')); + $this->assertEquals('/?r=post/view&id=1&title=sample+post', $url); + + // default setting with '/test/' as base url + $manager = new UrlManager(array( + 'baseUrl' => '/test/', + 'cache' => null, + )); + $url = $manager->createUrl('post/view', array('id' => 1, 'title' => 'sample post')); + $this->assertEquals('/test/?r=post/view&id=1&title=sample+post', $url); + + // pretty URL without rules + $manager = new UrlManager(array( + 'enablePrettyUrl' => true, + 'baseUrl' => '/', + 'cache' => null, + )); + $url = $manager->createUrl('post/view', array('id' => 1, 'title' => 'sample post')); + $this->assertEquals('/post/view?id=1&title=sample+post', $url); + $manager = new UrlManager(array( + 'enablePrettyUrl' => true, + 'baseUrl' => '/test/', + 'cache' => null, + )); + $url = $manager->createUrl('post/view', array('id' => 1, 'title' => 'sample post')); + $this->assertEquals('/test/post/view?id=1&title=sample+post', $url); + $manager = new UrlManager(array( + 'enablePrettyUrl' => true, + 'baseUrl' => '/test/index.php', + 'cache' => null, + )); + $url = $manager->createUrl('post/view', array('id' => 1, 'title' => 'sample post')); + $this->assertEquals('/test/index.php/post/view?id=1&title=sample+post', $url); + + // todo: test showScriptName + + // pretty URL with rules + $manager = new UrlManager(array( + 'enablePrettyUrl' => true, + 'cache' => null, + 'rules' => array( + array( + 'pattern' => 'post/<id>/<title>', + 'route' => 'post/view', + ), + ), + 'baseUrl' => '/', + )); + $url = $manager->createUrl('post/view', array('id' => 1, 'title' => 'sample post')); + $this->assertEquals('/post/1/sample+post', $url); + $url = $manager->createUrl('post/index', array('page' => 1)); + $this->assertEquals('/post/index?page=1', $url); + + // pretty URL with rules and suffix + $manager = new UrlManager(array( + 'enablePrettyUrl' => true, + 'cache' => null, + 'rules' => array( + array( + 'pattern' => 'post/<id>/<title>', + 'route' => 'post/view', + ), + ), + 'baseUrl' => '/', + 'suffix' => '.html', + )); + $url = $manager->createUrl('post/view', array('id' => 1, 'title' => 'sample post')); + $this->assertEquals('/post/1/sample+post.html', $url); + $url = $manager->createUrl('post/index', array('page' => 1)); + $this->assertEquals('/post/index.html?page=1', $url); + } + + public function testCreateAbsoluteUrl() + { + $manager = new UrlManager(array( + 'baseUrl' => '/', + 'hostInfo' => 'http://www.example.com', + 'cache' => null, + )); + $url = $manager->createAbsoluteUrl('post/view', array('id' => 1, 'title' => 'sample post')); + $this->assertEquals('http://www.example.com/?r=post/view&id=1&title=sample+post', $url); + } + + public function testParseRequest() + { + $manager = new UrlManager(array( + 'cache' => null, + )); + $request = new Request; + + // default setting without 'r' param + unset($_GET['r']); + $result = $manager->parseRequest($request); + $this->assertEquals(array('', array()), $result); + + // default setting with 'r' param + $_GET['r'] = 'site/index'; + $result = $manager->parseRequest($request); + $this->assertEquals(array('site/index', array()), $result); + + // default setting with 'r' param as an array + $_GET['r'] = array('site/index'); + $result = $manager->parseRequest($request); + $this->assertEquals(array('', array()), $result); + + // pretty URL without rules + $manager = new UrlManager(array( + 'enablePrettyUrl' => true, + 'cache' => null, + )); + // empty pathinfo + $request->pathInfo = ''; + $result = $manager->parseRequest($request); + $this->assertEquals(array('', array()), $result); + // normal pathinfo + $request->pathInfo = 'site/index'; + $result = $manager->parseRequest($request); + $this->assertEquals(array('site/index', array()), $result); + // pathinfo with module + $request->pathInfo = 'module/site/index'; + $result = $manager->parseRequest($request); + $this->assertEquals(array('module/site/index', array()), $result); + // pathinfo with trailing slashes + $request->pathInfo = 'module/site/index/'; + $result = $manager->parseRequest($request); + $this->assertEquals(array('module/site/index', array()), $result); + + // pretty URL rules + $manager = new UrlManager(array( + 'enablePrettyUrl' => true, + 'cache' => null, + 'rules' => array( + array( + 'pattern' => 'post/<id>/<title>', + 'route' => 'post/view', + ), + ), + )); + // matching pathinfo + $request->pathInfo = 'post/123/this+is+sample'; + $result = $manager->parseRequest($request); + $this->assertEquals(array('post/view', array('id' => '123', 'title' => 'this+is+sample')), $result); + // matching pathinfo with trailing slashes + $request->pathInfo = 'post/123/this+is+sample/'; + $result = $manager->parseRequest($request); + $this->assertEquals(array('post/view', array('id' => '123', 'title' => 'this+is+sample')), $result); + // empty pathinfo + $request->pathInfo = ''; + $result = $manager->parseRequest($request); + $this->assertEquals(array('', array()), $result); + // normal pathinfo + $request->pathInfo = 'site/index'; + $result = $manager->parseRequest($request); + $this->assertEquals(array('site/index', array()), $result); + // pathinfo with module + $request->pathInfo = 'module/site/index'; + $result = $manager->parseRequest($request); + $this->assertEquals(array('module/site/index', array()), $result); + + // pretty URL rules + $manager = new UrlManager(array( + 'enablePrettyUrl' => true, + 'suffix' => '.html', + 'cache' => null, + 'rules' => array( + array( + 'pattern' => 'post/<id>/<title>', + 'route' => 'post/view', + ), + ), + )); + // matching pathinfo + $request->pathInfo = 'post/123/this+is+sample.html'; + $result = $manager->parseRequest($request); + $this->assertEquals(array('post/view', array('id' => '123', 'title' => 'this+is+sample')), $result); + // matching pathinfo without suffix + $request->pathInfo = 'post/123/this+is+sample'; + $result = $manager->parseRequest($request); + $this->assertFalse($result); + // empty pathinfo + $request->pathInfo = ''; + $result = $manager->parseRequest($request); + $this->assertEquals(array('', array()), $result); + // normal pathinfo + $request->pathInfo = 'site/index.html'; + $result = $manager->parseRequest($request); + $this->assertEquals(array('site/index', array()), $result); + // pathinfo without suffix + $request->pathInfo = 'site/index'; + $result = $manager->parseRequest($request); + $this->assertFalse($result); + } +} diff --git a/tests/unit/framework/web/UrlRuleTest.php b/tests/unit/framework/web/UrlRuleTest.php new file mode 100644 index 0000000..825199e --- /dev/null +++ b/tests/unit/framework/web/UrlRuleTest.php @@ -0,0 +1,615 @@ +<?php + +namespace yiiunit\framework\web; + +use yii\web\UrlManager; +use yii\web\UrlRule; +use yii\web\Request; + +class UrlRuleTest extends \yiiunit\TestCase +{ + public function testCreateUrl() + { + $manager = new UrlManager(array('cache' => null)); + $suites = $this->getTestsForCreateUrl(); + foreach ($suites as $i => $suite) { + list ($name, $config, $tests) = $suite; + $rule = new UrlRule($config); + foreach ($tests as $j => $test) { + list ($route, $params, $expected) = $test; + $url = $rule->createUrl($manager, $route, $params); + $this->assertEquals($expected, $url, "Test#$i-$j: $name"); + } + } + } + + public function testParseRequest() + { + $manager = new UrlManager(array('cache' => null)); + $request = new Request; + $suites = $this->getTestsForParseRequest(); + foreach ($suites as $i => $suite) { + list ($name, $config, $tests) = $suite; + $rule = new UrlRule($config); + foreach ($tests as $j => $test) { + $request->pathInfo = $test[0]; + $route = $test[1]; + $params = isset($test[2]) ? $test[2] : array(); + $result = $rule->parseRequest($manager, $request); + if ($route === false) { + $this->assertFalse($result, "Test#$i-$j: $name"); + } else { + $this->assertEquals(array($route, $params), $result, "Test#$i-$j: $name"); + } + } + } + } + + protected function getTestsForCreateUrl() + { + // structure of each test + // message for the test + // config for the URL rule + // list of inputs and outputs + // route + // params + // expected output + return array( + array( + 'empty pattern', + array( + 'pattern' => '', + 'route' => 'post/index', + ), + array( + array('post/index', array(), ''), + array('comment/index', array(), false), + array('post/index', array('page' => 1), '?page=1'), + ), + ), + array( + 'without param', + array( + 'pattern' => 'posts', + 'route' => 'post/index', + ), + array( + array('post/index', array(), 'posts'), + array('comment/index', array(), false), + array('post/index', array('page' => 1), 'posts?page=1'), + ), + ), + array( + 'parsing only', + array( + 'pattern' => 'posts', + 'route' => 'post/index', + 'mode' => UrlRule::PARSING_ONLY, + ), + array( + array('post/index', array(), false), + ), + ), + array( + 'with param', + array( + 'pattern' => 'post/<page>', + 'route' => 'post/index', + ), + array( + array('post/index', array(), false), + array('comment/index', array(), false), + array('post/index', array('page' => 1), 'post/1'), + array('post/index', array('page' => 1, 'tag' => 'a'), 'post/1?tag=a'), + ), + ), + array( + 'with param requirement', + array( + 'pattern' => 'post/<page:\d+>', + 'route' => 'post/index', + ), + array( + array('post/index', array('page' => 'abc'), false), + array('post/index', array('page' => 1), 'post/1'), + array('post/index', array('page' => 1, 'tag' => 'a'), 'post/1?tag=a'), + ), + ), + array( + 'with multiple params', + array( + 'pattern' => 'post/<page:\d+>-<tag>', + 'route' => 'post/index', + ), + array( + array('post/index', array('page' => '1abc'), false), + array('post/index', array('page' => 1), false), + array('post/index', array('page' => 1, 'tag' => 'a'), 'post/1-a'), + ), + ), + array( + 'with optional param', + array( + 'pattern' => 'post/<page:\d+>/<tag>', + 'route' => 'post/index', + 'defaults' => array('page' => 1), + ), + array( + array('post/index', array('page' => 1), false), + array('post/index', array('page' => '1abc', 'tag' => 'a'), false), + array('post/index', array('page' => 1, 'tag' => 'a'), 'post/a'), + array('post/index', array('page' => 2, 'tag' => 'a'), 'post/2/a'), + ), + ), + array( + 'with optional param not in pattern', + array( + 'pattern' => 'post/<tag>', + 'route' => 'post/index', + 'defaults' => array('page' => 1), + ), + array( + array('post/index', array('page' => 1), false), + array('post/index', array('page' => '1abc', 'tag' => 'a'), false), + array('post/index', array('page' => 2, 'tag' => 'a'), false), + array('post/index', array('page' => 1, 'tag' => 'a'), 'post/a'), + ), + ), + array( + 'multiple optional params', + array( + 'pattern' => 'post/<page:\d+>/<tag>/<sort:yes|no>', + 'route' => 'post/index', + 'defaults' => array('page' => 1, 'sort' => 'yes'), + ), + array( + array('post/index', array('page' => 1), false), + array('post/index', array('page' => '1abc', 'tag' => 'a'), false), + array('post/index', array('page' => 1, 'tag' => 'a', 'sort' => 'YES'), false), + array('post/index', array('page' => 1, 'tag' => 'a', 'sort' => 'yes'), 'post/a'), + array('post/index', array('page' => 2, 'tag' => 'a', 'sort' => 'yes'), 'post/2/a'), + array('post/index', array('page' => 2, 'tag' => 'a', 'sort' => 'no'), 'post/2/a/no'), + array('post/index', array('page' => 1, 'tag' => 'a', 'sort' => 'no'), 'post/a/no'), + ), + ), + array( + 'optional param and required param separated by dashes', + array( + 'pattern' => 'post/<page:\d+>-<tag>', + 'route' => 'post/index', + 'defaults' => array('page' => 1), + ), + array( + array('post/index', array('page' => 1), false), + array('post/index', array('page' => '1abc', 'tag' => 'a'), false), + array('post/index', array('page' => 1, 'tag' => 'a'), 'post/-a'), + array('post/index', array('page' => 2, 'tag' => 'a'), 'post/2-a'), + ), + ), + array( + 'optional param at the end', + array( + 'pattern' => 'post/<tag>/<page:\d+>', + 'route' => 'post/index', + 'defaults' => array('page' => 1), + ), + array( + array('post/index', array('page' => 1), false), + array('post/index', array('page' => '1abc', 'tag' => 'a'), false), + array('post/index', array('page' => 1, 'tag' => 'a'), 'post/a'), + array('post/index', array('page' => 2, 'tag' => 'a'), 'post/a/2'), + ), + ), + array( + 'consecutive optional params', + array( + 'pattern' => 'post/<page:\d+>/<tag>', + 'route' => 'post/index', + 'defaults' => array('page' => 1, 'tag' => 'a'), + ), + array( + array('post/index', array('page' => 1), false), + array('post/index', array('page' => '1abc', 'tag' => 'a'), false), + array('post/index', array('page' => 1, 'tag' => 'a'), 'post'), + array('post/index', array('page' => 2, 'tag' => 'a'), 'post/2'), + array('post/index', array('page' => 1, 'tag' => 'b'), 'post/b'), + array('post/index', array('page' => 2, 'tag' => 'b'), 'post/2/b'), + ), + ), + array( + 'consecutive optional params separated by dash', + array( + 'pattern' => 'post/<page:\d+>-<tag>', + 'route' => 'post/index', + 'defaults' => array('page' => 1, 'tag' => 'a'), + ), + array( + array('post/index', array('page' => 1), false), + array('post/index', array('page' => '1abc', 'tag' => 'a'), false), + array('post/index', array('page' => 1, 'tag' => 'a'), 'post/-'), + array('post/index', array('page' => 1, 'tag' => 'b'), 'post/-b'), + array('post/index', array('page' => 2, 'tag' => 'a'), 'post/2-'), + array('post/index', array('page' => 2, 'tag' => 'b'), 'post/2-b'), + ), + ), + array( + 'route has parameters', + array( + 'pattern' => '<controller>/<action>', + 'route' => '<controller>/<action>', + 'defaults' => array(), + ), + array( + array('post/index', array('page' => 1), 'post/index?page=1'), + array('module/post/index', array(), false), + ), + ), + array( + 'route has parameters with regex', + array( + 'pattern' => '<controller:post|comment>/<action>', + 'route' => '<controller>/<action>', + 'defaults' => array(), + ), + array( + array('post/index', array('page' => 1), 'post/index?page=1'), + array('comment/index', array('page' => 1), 'comment/index?page=1'), + array('test/index', array('page' => 1), false), + array('post', array(), false), + array('module/post/index', array(), false), + array('post/index', array('controller' => 'comment'), 'post/index?controller=comment'), + ), + ), + array( + 'route has default parameter', + array( + 'pattern' => '<controller:post|comment>/<action>', + 'route' => '<controller>/<action>', + 'defaults' => array('action' => 'index'), + ), + array( + array('post/view', array('page' => 1), 'post/view?page=1'), + array('comment/view', array('page' => 1), 'comment/view?page=1'), + array('test/view', array('page' => 1), false), + array('test/index', array('page' => 1), false), + array('post/index', array('page' => 1), 'post?page=1'), + ), + ), + array( + 'empty pattern with suffix', + array( + 'pattern' => '', + 'route' => 'post/index', + 'suffix' => '.html', + ), + array( + array('post/index', array(), ''), + array('comment/index', array(), false), + array('post/index', array('page' => 1), '?page=1'), + ), + ), + array( + 'regular pattern with suffix', + array( + 'pattern' => 'posts', + 'route' => 'post/index', + 'suffix' => '.html', + ), + array( + array('post/index', array(), 'posts.html'), + array('comment/index', array(), false), + array('post/index', array('page' => 1), 'posts.html?page=1'), + ), + ), + array( + 'empty pattern with slash suffix', + array( + 'pattern' => '', + 'route' => 'post/index', + 'suffix' => '/', + ), + array( + array('post/index', array(), ''), + array('comment/index', array(), false), + array('post/index', array('page' => 1), '?page=1'), + ), + ), + array( + 'regular pattern with slash suffix', + array( + 'pattern' => 'posts', + 'route' => 'post/index', + 'suffix' => '/', + ), + array( + array('post/index', array(), 'posts/'), + array('comment/index', array(), false), + array('post/index', array('page' => 1), 'posts/?page=1'), + ), + ), + ); + } + + protected function getTestsForParseRequest() + { + // structure of each test + // message for the test + // config for the URL rule + // list of inputs and outputs + // pathInfo + // expected route, or false if the rule doesn't apply + // expected params, or not set if empty + return array( + array( + 'empty pattern', + array( + 'pattern' => '', + 'route' => 'post/index', + ), + array( + array('', 'post/index'), + array('a', false), + ), + ), + array( + 'without param', + array( + 'pattern' => 'posts', + 'route' => 'post/index', + ), + array( + array('posts', 'post/index'), + array('a', false), + ), + ), + array( + 'creation only', + array( + 'pattern' => 'posts', + 'route' => 'post/index', + 'mode' => UrlRule::CREATION_ONLY, + ), + array( + array('posts', false), + ), + ), + array( + 'with param', + array( + 'pattern' => 'post/<page>', + 'route' => 'post/index', + ), + array( + array('post/1', 'post/index', array('page' => '1')), + array('post/a', 'post/index', array('page' => 'a')), + array('post', false), + array('posts', false), + ), + ), + array( + 'with param requirement', + array( + 'pattern' => 'post/<page:\d+>', + 'route' => 'post/index', + ), + array( + array('post/1', 'post/index', array('page' => '1')), + array('post/a', false), + array('post/1/a', false), + ), + ), + array( + 'with multiple params', + array( + 'pattern' => 'post/<page:\d+>-<tag>', + 'route' => 'post/index', + ), + array( + array('post/1-a', 'post/index', array('page' => '1', 'tag' => 'a')), + array('post/a', false), + array('post/1', false), + array('post/1/a', false), + ), + ), + array( + 'with optional param', + array( + 'pattern' => 'post/<page:\d+>/<tag>', + 'route' => 'post/index', + 'defaults' => array('page' => 1), + ), + array( + array('post/1/a', 'post/index', array('page' => '1', 'tag' => 'a')), + array('post/2/a', 'post/index', array('page' => '2', 'tag' => 'a')), + array('post/a', 'post/index', array('page' => '1', 'tag' => 'a')), + array('post/1', 'post/index', array('page' => '1', 'tag' => '1')), + ), + ), + array( + 'with optional param not in pattern', + array( + 'pattern' => 'post/<tag>', + 'route' => 'post/index', + 'defaults' => array('page' => 1), + ), + array( + array('post/a', 'post/index', array('page' => '1', 'tag' => 'a')), + array('post/1', 'post/index', array('page' => '1', 'tag' => '1')), + array('post', false), + ), + ), + array( + 'multiple optional params', + array( + 'pattern' => 'post/<page:\d+>/<tag>/<sort:yes|no>', + 'route' => 'post/index', + 'defaults' => array('page' => 1, 'sort' => 'yes'), + ), + array( + array('post/1/a/yes', 'post/index', array('page' => '1', 'tag' => 'a', 'sort' => 'yes')), + array('post/2/a/no', 'post/index', array('page' => '2', 'tag' => 'a', 'sort' => 'no')), + array('post/2/a', 'post/index', array('page' => '2', 'tag' => 'a', 'sort' => 'yes')), + array('post/a/no', 'post/index', array('page' => '1', 'tag' => 'a', 'sort' => 'no')), + array('post/a', 'post/index', array('page' => '1', 'tag' => 'a', 'sort' => 'yes')), + array('post', false), + ), + ), + array( + 'optional param and required param separated by dashes', + array( + 'pattern' => 'post/<page:\d+>-<tag>', + 'route' => 'post/index', + 'defaults' => array('page' => 1), + ), + array( + array('post/1-a', 'post/index', array('page' => '1', 'tag' => 'a')), + array('post/2-a', 'post/index', array('page' => '2', 'tag' => 'a')), + array('post/-a', 'post/index', array('page' => '1', 'tag' => 'a')), + array('post/a', false), + array('post-a', false), + ), + ), + array( + 'optional param at the end', + array( + 'pattern' => 'post/<tag>/<page:\d+>', + 'route' => 'post/index', + 'defaults' => array('page' => 1), + ), + array( + array('post/a/1', 'post/index', array('page' => '1', 'tag' => 'a')), + array('post/a/2', 'post/index', array('page' => '2', 'tag' => 'a')), + array('post/a', 'post/index', array('page' => '1', 'tag' => 'a')), + array('post/2', 'post/index', array('page' => '1', 'tag' => '2')), + array('post', false), + ), + ), + array( + 'consecutive optional params', + array( + 'pattern' => 'post/<page:\d+>/<tag>', + 'route' => 'post/index', + 'defaults' => array('page' => 1, 'tag' => 'a'), + ), + array( + array('post/2/b', 'post/index', array('page' => '2', 'tag' => 'b')), + array('post/2', 'post/index', array('page' => '2', 'tag' => 'a')), + array('post', 'post/index', array('page' => '1', 'tag' => 'a')), + array('post/b', 'post/index', array('page' => '1', 'tag' => 'b')), + array('post//b', false), + ), + ), + array( + 'consecutive optional params separated by dash', + array( + 'pattern' => 'post/<page:\d+>-<tag>', + 'route' => 'post/index', + 'defaults' => array('page' => 1, 'tag' => 'a'), + ), + array( + array('post/2-b', 'post/index', array('page' => '2', 'tag' => 'b')), + array('post/2-', 'post/index', array('page' => '2', 'tag' => 'a')), + array('post/-b', 'post/index', array('page' => '1', 'tag' => 'b')), + array('post/-', 'post/index', array('page' => '1', 'tag' => 'a')), + array('post', false), + ), + ), + array( + 'route has parameters', + array( + 'pattern' => '<controller>/<action>', + 'route' => '<controller>/<action>', + 'defaults' => array(), + ), + array( + array('post/index', 'post/index'), + array('module/post/index', false), + ), + ), + array( + 'route has parameters with regex', + array( + 'pattern' => '<controller:post|comment>/<action>', + 'route' => '<controller>/<action>', + 'defaults' => array(), + ), + array( + array('post/index', 'post/index'), + array('comment/index', 'comment/index'), + array('test/index', false), + array('post', false), + array('module/post/index', false), + ), + ), + array( + 'route has default parameter', + array( + 'pattern' => '<controller:post|comment>/<action>', + 'route' => '<controller>/<action>', + 'defaults' => array('action' => 'index'), + ), + array( + array('post/view', 'post/view'), + array('comment/view', 'comment/view'), + array('test/view', false), + array('post', 'post/index'), + array('posts', false), + array('test', false), + array('index', false), + ), + ), + array( + 'empty pattern with suffix', + array( + 'pattern' => '', + 'route' => 'post/index', + 'suffix' => '.html', + ), + array( + array('', 'post/index'), + array('.html', false), + array('a.html', false), + ), + ), + array( + 'regular pattern with suffix', + array( + 'pattern' => 'posts', + 'route' => 'post/index', + 'suffix' => '.html', + ), + array( + array('posts.html', 'post/index'), + array('posts', false), + array('posts.HTML', false), + array('a.html', false), + array('a', false), + ), + ), + array( + 'empty pattern with slash suffix', + array( + 'pattern' => '', + 'route' => 'post/index', + 'suffix' => '/', + ), + array( + array('', 'post/index'), + array('a', false), + ), + ), + array( + 'regular pattern with slash suffix', + array( + 'pattern' => 'posts', + 'route' => 'post/index', + 'suffix' => '/', + ), + array( + array('posts', 'post/index'), + array('a', false), + ), + ), + ); + } +} diff --git a/tests/unit/runtime/.gitignore b/tests/unit/runtime/.gitignore new file mode 100644 index 0000000..72e8ffc --- /dev/null +++ b/tests/unit/runtime/.gitignore @@ -0,0 +1 @@ +* diff --git a/tests/web/app/assets/.gitignore b/tests/web/app/assets/.gitignore new file mode 100644 index 0000000..72e8ffc --- /dev/null +++ b/tests/web/app/assets/.gitignore @@ -0,0 +1 @@ +* diff --git a/tests/web/app/index.php b/tests/web/app/index.php new file mode 100644 index 0000000..4cfa1ab --- /dev/null +++ b/tests/web/app/index.php @@ -0,0 +1,6 @@ +<?php + +require(__DIR__ . '/../../../framework/yii.php'); + +$application = new yii\web\Application('test', __DIR__ . '/protected'); +$application->run(); diff --git a/tests/web/app/protected/config/main.php b/tests/web/app/protected/config/main.php new file mode 100644 index 0000000..eed6d54 --- /dev/null +++ b/tests/web/app/protected/config/main.php @@ -0,0 +1,3 @@ +<?php + +return array(); \ No newline at end of file diff --git a/tests/web/app/protected/controllers/SiteController.php b/tests/web/app/protected/controllers/SiteController.php new file mode 100644 index 0000000..050bf90 --- /dev/null +++ b/tests/web/app/protected/controllers/SiteController.php @@ -0,0 +1,30 @@ +<?php + +use yii\helpers\Html; + +class DefaultController extends \yii\web\Controller +{ + public function actionIndex() + { + echo 'hello world'; + } + + public function actionForm() + { + echo Html::beginForm(); + echo Html::checkboxList('test', array( + 'value 1' => 'item 1', + 'value 2' => 'item 2', + 'value 3' => 'item 3', + ), isset($_POST['test']) ? $_POST['test'] : null, + function ($index, $label, $name, $value, $checked) { + return Html::label( + $label . ' ' . Html::checkbox($name, $value, $checked), + null, array('class' => 'inline checkbox') + ); + }); + echo Html::submitButton(); + echo Html::endForm(); + print_r($_POST); + } +} \ No newline at end of file diff --git a/tests/web/app/protected/runtime/.gitignore b/tests/web/app/protected/runtime/.gitignore new file mode 100644 index 0000000..72e8ffc --- /dev/null +++ b/tests/web/app/protected/runtime/.gitignore @@ -0,0 +1 @@ +* diff --git a/tests/web/app/protected/views/site/index.php b/tests/web/app/protected/views/site/index.php new file mode 100644 index 0000000..5decb56 --- /dev/null +++ b/tests/web/app/protected/views/site/index.php @@ -0,0 +1,8 @@ +<?php +/** + * Created by JetBrains PhpStorm. + * User: qiang + * Date: 3/16/13 + * Time: 10:41 AM + * To change this template use File | Settings | File Templates. + */ \ No newline at end of file diff --git a/todo.md b/todo.md index 60e37c5..f66d3c1 100644 --- a/todo.md +++ b/todo.md @@ -1,33 +1,20 @@ -- db - * pgsql, sql server, oracle, db2 drivers - * unit tests on different DB drivers - * document-based (should allow storage-specific methods additionally to generic ones) - * mongodb (put it under framework/db/mongodb) - * key-value-based (should allow storage-specific methods additionally to generic ones) - * redis (put it under framework/db/redis or perhaps framework/caching?) -- base - * TwigViewRenderer - * SmartyViewRenderer -- logging - * WebTarget (TBD after web is in place): should consider using javascript and make it into a toolbar - * ProfileTarget (TBD after web is in place): should consider using javascript and make it into a toolbar - * unit tests - caching - * a console command to clear cached data - * unit tests + * dependency unit tests - validators + * Refactor validators to add validateValue() for every validator, if possible. Check if value is an array. * FileValidator: depends on CUploadedFile * CaptchaValidator: depends on CaptchaAction * DateValidator: should we use CDateTimeParser, or simply use strtotime()? * CompareValidator::clientValidateAttribute(): depends on CHtml::activeId() +memo + * Minimal PHP version required: 5.3.7 (http://www.php.net/manual/en/function.crypt.php) --- - base * module - Module should be able to define its own configuration including routes. Application should be able to overwrite it. * application - * security - built-in console commands + api doc builder * support for markdown syntax @@ -35,12 +22,10 @@ * consider to be released as a separate tool for user app docs - i18n * consider using PHP built-in support and data - * message translations, choice format * formatting: number and date * parsing?? * make dates/date patterns uniform application-wide including JUI, formats etc. - helpers - * array * image * string * file @@ -53,8 +38,6 @@ * move generation API out of gii, provide yiic commands to use it. Use same templates for gii/yiic. * i18n variant of templates * allow to generate module-specific CRUD -- markup and HTML helpers - * use HTML5 instead of XHTML - assets * ability to manage scripts order (store these in a vector?) * http://ryanbigg.com/guides/asset_pipeline.html, http://guides.rubyonrails.org/asset_pipeline.html, use content hash instead of mtime + directory hash.