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..fcf643d 100644 --- a/docs/code_style.md +++ b/docs/code_style.md @@ -251,10 +251,8 @@ switch ($this->phpType) { ~~~ class name or directory + private static $_imported = array(); // alias => class name or directory private static $_logger; /** @@ -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); } } @@ -275,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'; } } @@ -298,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; diff --git a/framework/base/Action.php b/framework/base/Action.php index f72aa1b..7142539 100644 --- a/framework/base/Action.php +++ b/framework/base/Action.php @@ -1,9 +1,7 @@ + * @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 55dc0cc..fd2ecad 100644 --- a/framework/base/Application.php +++ b/framework/base/Application.php @@ -1,16 +1,14 @@ 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()); } @@ -142,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) @@ -155,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; @@ -235,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. @@ -293,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. */ @@ -320,12 +363,21 @@ 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 getViewRenderer() + public function getUrlManager() { - return $this->getComponent('viewRenderer'); + return $this->getComponent('urlManager'); } /** @@ -343,8 +395,6 @@ class Application extends Module public function registerDefaultAliases() { Yii::$aliases['@app'] = $this->getBasePath(); - Yii::$aliases['@entry'] = dirname($_SERVER['SCRIPT_FILENAME']); - Yii::$aliases['@www'] = ''; } /** @@ -360,8 +410,11 @@ class Application extends Module 'i18n' => array( 'class' => 'yii\i18n\I18N', ), - 'securityManager' => array( - 'class' => 'yii\base\SecurityManager', + 'urlManager' => array( + 'class' => 'yii\web\UrlManager', + ), + 'view' => array( + 'class' => 'yii\base\View', ), )); } @@ -375,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; } } @@ -409,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); 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 @@ 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); } + /** + * 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 $this->createView()->renderFile($file, $params); - } - - public function createView() - { - return new View($this); + return Yii::$app->getView()->renderFile($file, $params, $this); } /** @@ -337,4 +348,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..9343d68 100644 --- a/framework/base/Dictionary.php +++ b/framework/base/Dictionary.php @@ -1,15 +1,13 @@ 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.'); } } 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 @@ + * @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 9464a92..f71b8c8 100644 --- a/framework/base/ErrorHandler.php +++ b/framework/base/ErrorHandler.php @@ -1,9 +1,7 @@ * @since 2.0 */ -use yii\util\VarDumper; +use yii\helpers\VarDumper; class ErrorHandler extends Component { @@ -80,7 +78,7 @@ class ErrorHandler extends Component if (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] === 'XMLHttpRequest') { \Yii::$app->renderException($exception); } else { - $view = new View($this); + $view = new View; if (!YII_DEBUG || $exception instanceof UserException) { $viewName = $this->errorView; } else { @@ -88,7 +86,7 @@ class ErrorHandler extends Component } echo $view->render($viewName, array( 'exception' => $exception, - )); + ), $this); } } else { \Yii::$app->renderException($exception); @@ -255,15 +253,10 @@ class ErrorHandler extends Component */ public function renderAsHtml($exception) { - $view = new View($this); - if (!YII_DEBUG || $exception instanceof UserException) { - $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..4ba57b2 100644 --- a/framework/base/Event.php +++ b/framework/base/Event.php @@ -1,9 +1,7 @@ + * @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 377abbd..6663e29 100644 --- a/framework/base/InvalidRequestException.php +++ b/framework/base/InvalidRequestException.php @@ -1,9 +1,7 @@ _errors)) { + return array(); + } else { + $errors = array(); + foreach ($this->_errors as $errors) { + if (isset($errors[0])) { + $errors[] = $errors[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__); } } 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 @@ $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 @@ * @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 @@ - * @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, array('rijndael-256', '', 'ofb', ''). - * - * 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::$app->getGlobalState(self::STATE_VALIDATION_KEY)) !== null) { - $this->setValidationKey($key); - } else { - $key = $this->generateRandomKey(); - $this->setValidationKey($key); - \Yii::$app->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::$app->getGlobalState(self::STATE_ENCRYPTION_KEY)) !== null) { - $this->setEncryptionKey($key); - } else { - $key = $this->generateRandomKey(); - $this->setEncryptionKey($key); - \Yii::$app->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 52d5245..88ecb0a 100644 --- a/framework/base/Theme.php +++ b/framework/base/Theme.php @@ -1,9 +1,7 @@ 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 b46903f..29bedca 100644 --- a/framework/base/UnknownMethodException.php +++ b/framework/base/UnknownMethodException.php @@ -1,9 +1,7 @@ = 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); } } @@ -242,7 +240,7 @@ 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) { @@ -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 @@ * @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; - /** - * @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. - */ - public $layout; + public $context; /** - * @var string the language that the view should be rendered in. If not set, it will use - * the value of [[Application::language]]. + * @var mixed custom parameters that are shared among view templates. */ - public $language; + public $params; /** - * @var string the language that the original view is in. If not set, it will use - * the value of [[Application::sourceLanguage]]. + * @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 $sourceLanguage; + public $renderer; /** - * @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 Theme|array the theme object or the configuration array for creating the theme. + * If not set, it means theming is not enabled. */ - public $enableI18N = true; + public $theme; /** - * @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 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 $enableTheme = true; + public $clips; /** - * @var mixed custom parameters that are available in the view template + * @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 $params; - + public $widgetStack = array(); /** - * @var Widget[] the widgets that are currently not ended + * @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. */ - private $_widgetStack = array(); - + public $cacheStack = 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 + * @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 function __construct($owner, $config = array()) - { - $this->owner = $owner; - parent::__construct($config); - } + public $dynamicPlaceholders = array(); - /** - * 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. + * + * The method will call [[FileHelper::localize()]] to localize the view file. * - * @param string $file 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::$app->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 { + 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 { - return $this->renderPhpFile($file, $params); + $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 = ""; + $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. "@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 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. "@app/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::$app->getViewPath() . DIRECTORY_SEPARATOR . $view; - } - } elseif (strncmp($view, '//', 2) !== 0 && Yii::$app->controller !== null) { - // e.g. "/site/index" - $file = Yii::$app->controller->module->getViewPath() . DIRECTORY_SEPARATOR . ltrim($view, '/'); - } else { - // e.g. "//layouts/main" - $file = Yii::$app->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::$app->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. "@app/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::$app->controller) { - $module = Yii::$app->controller->module; - } else { - $module = Yii::$app; - } - $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::$app->getLayoutPath() . DIRECTORY_SEPARATOR . $view; } else { - $file = $module->getLayoutPath() . DIRECTORY_SEPARATOR . $view; + return true; } + } - if (is_file($file)) { - if ($this->enableTheme && ($theme = Yii::$app->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 @@ 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 @@ 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 @@ * @since 2.0 @@ -29,23 +27,25 @@ class DbDependency extends Dependency */ public $connectionID = '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); } @@ -68,22 +68,23 @@ class DbDependency extends Dependency protected function generateDependencyData() { $db = $this->getDb(); - /** - * @var \yii\db\Command $command - */ - $command = $this->query->createCommand($db); 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; } /** + * @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. @@ -91,11 +92,11 @@ class DbDependency extends Dependency public function getDb() { if ($this->_db === null) { - $db = \Yii::$app->getComponent($this->connectionID); + $db = Yii::$app->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."); + throw new InvalidConfigException("DbCacheDependency::connectionID must refer to the ID of a DB application component."); } } return $this->_db; 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 @@ 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 @@ getRequest(); if ($request->getIsConsoleRequest()) { - return $this->runAction($request->route, $request->params); + list ($route, $params) = $request->resolve(); + return $this->runAction($route, $params); } else { - throw new Exception(\Yii::t('yii|this script must be run from the command line.')); + throw new Exception(\Yii::t('yii|This script must be run from the command line.')); } } @@ -126,7 +127,7 @@ class Application extends \yii\base\Application 'message' => 'yii\console\controllers\MessageController', 'help' => 'yii\console\controllers\HelpController', 'migrate' => 'yii\console\controllers\MigrateController', - 'app' => 'yii\console\controllers\CreateController', + 'app' => 'yii\console\controllers\AppController', 'cache' => 'yii\console\controllers\CacheController', ); } diff --git a/framework/console/Controller.php b/framework/console/Controller.php index ff84a45..b9b0523 100644 --- a/framework/console/Controller.php +++ b/framework/console/Controller.php @@ -1,9 +1,7 @@ resolveRequest(); - } - 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(self::ANONYMOUS_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[self::ANONYMOUS_PARAMS][] = $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 91% 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 @@ * @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 index 866db12..6765f9b 100644 --- a/framework/console/controllers/CacheController.php +++ b/framework/console/controllers/CacheController.php @@ -1,9 +1,7 @@ * @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 59dcb3f..7f9a18f 100644 --- a/framework/console/controllers/MigrateController.php +++ b/framework/console/controllers/MigrateController.php @@ -1,10 +1,8 @@ * @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,7 +13,7 @@ use yii\console\Exception; use yii\console\Controller; use yii\db\Connection; use yii\db\Query; -use yii\util\ArrayHelper; +use yii\helpers\ArrayHelper; /** * This command manages application migrations. @@ -116,7 +114,7 @@ class MigrateController extends Controller { if (parent::beforeAction($action)) { $path = Yii::getAlias($this->migrationPath); - if ($path === false || !is_dir($path)) { + if (!is_dir($path)) { throw new Exception("The migration directory \"{$this->migrationPath}\" does not exist."); } $this->migrationPath = $path; diff --git a/framework/console/create/config.php b/framework/console/webapp/config.php similarity index 83% 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 @@ * @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 0b2451f..0c15121 100644 --- a/framework/db/ActiveRecord.php +++ b/framework/db/ActiveRecord.php @@ -1,24 +1,22 @@ * @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. @@ -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 54c6c62..f1b198b 100644 --- a/framework/db/ActiveRelation.php +++ b/framework/db/ActiveRelation.php @@ -1,10 +1,8 @@ * @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 @@ 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()); } } } @@ -294,7 +292,7 @@ class Command extends \yii\base\Component \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()); } } @@ -391,7 +389,13 @@ class Command extends \yii\base\Component } if (isset($cache)) { - $cacheKey = $cache->buildKey(__CLASS__, $db->dsn, $db->username, $sql, $paramLog); + $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__); return $result; @@ -433,7 +437,7 @@ class Command extends \yii\base\Component $message = $e->getMessage(); \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..40164a3 100644 --- a/framework/db/Connection.php +++ b/framework/db/Connection.php @@ -1,9 +1,7 @@ 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 +330,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 @@ errorInfo = $errorInfo; - parent::__construct($message, $code); + parent::__construct($message, $code, $previous); } /** 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 @@ 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 ebca888..75375cc 100644 --- a/framework/db/QueryBuilder.php +++ b/framework/db/QueryBuilder.php @@ -1,9 +1,7 @@ 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)); @@ -592,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) { @@ -677,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]]. @@ -693,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; @@ -720,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) @@ -729,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 @@ -756,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.'); } } @@ -804,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); } /** @@ -827,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); } /** @@ -877,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) @@ -885,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 6306776..5fe6121 100644 --- a/framework/db/Schema.php +++ b/framework/db/Schema.php @@ -1,9 +1,7 @@ buildKey(__CLASS__, $this->db->dsn, $this->db->username, $name); + return $cache->buildKey(array( + __CLASS__, + $this->db->dsn, + $this->db->username, + $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 @@ 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 @@ 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 @@ 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 97% 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 @@ $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 98% rename from framework/util/FileHelper.php rename to framework/helpers/FileHelper.php index 996fba0..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); diff --git a/framework/helpers/Html.php b/framework/helpers/Html.php new file mode 100644 index 0000000..b004885 --- /dev/null +++ b/framework/helpers/Html.php @@ -0,0 +1,976 @@ + + * @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"; + } + } + + /** + * 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 ""; + } + + /** + * 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 ''; + } + + /** + * 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', "/**/", $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', "/**/", $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 ''; + } + + /** + * 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()]] which, if the string is an alias, + * will be resolved into a URL; + * - is an array: the first array element is considered a route, while the rest of the name-value + * pairs are considered as the parameters to be used for URL creation using [[\yii\base\Application::createUrl()]]. + * Here are some examples: `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])) { + return Yii::$app->createUrl($url[0], array_splice($url, 1)); + } 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 @@ + + * @author Tom Worster + * @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, " 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 71% 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 @@ '\1oves', '/(f)oot$/i' => '\1eet', '/(c)hild$/i' => '\1hildren', diff --git a/framework/helpers/VarDumper.php b/framework/helpers/VarDumper.php new file mode 100644 index 0000000..64c3639 --- /dev/null +++ b/framework/helpers/VarDumper.php @@ -0,0 +1,134 @@ + + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008-2011 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\helpers; + +/** + * VarDumper is intended to replace the buggy PHP function var_dump and print_r. + * It can correctly identify the recursively referenced objects in a complex + * object structure. It also has a recursive depth control to avoid indefinite + * recursive display of some peculiar variables. + * + * VarDumper can be used as follows, + * + * ~~~ + * VarDumper::dump($var); + * ~~~ + * + * @author Qiang Xue + * @since 2.0 + */ +class CVarDumper +{ + private static $_objects; + private static $_output; + private static $_depth; + + /** + * Displays a variable. + * This method achieves the similar functionality as var_dump and print_r + * but is more robust when handling complex objects such as Yii controllers. + * @param mixed $var variable to be dumped + * @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) + { + echo self::dumpAsString($var, $depth, $highlight); + } + + /** + * Dumps a variable in terms of a string. + * This method achieves the similar functionality as var_dump and print_r + * but is more robust when handling complex objects such as Yii controllers. + * @param mixed $var variable to be dumped + * @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 + * @return string the string representation of the variable + */ + 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("/', '', $result, 1); + } + return self::$_output; + } + + /* + * @param mixed $var variable to be dumped + * @param integer $level depth level + */ + private static function dumpInternal($var, $level) + { + switch (gettype($var)) { + case 'boolean': + self::$_output .= $var ? 'true' : 'false'; + break; + case 'integer': + self::$_output .= "$var"; + break; + case 'double': + self::$_output .= "$var"; + break; + case 'string': + self::$_output .= "'" . addslashes($var) . "'"; + break; + case 'resource': + self::$_output .= '{resource}'; + break; + case 'NULL': + self::$_output .= "null"; + break; + case 'unknown type': + self::$_output .= '{unknown}'; + break; + case 'array': + 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 . ')'; + } + break; + case 'object': + 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 . ')'; + } + break; + } + } +} \ No newline at end of file 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 index ab87dfc..0409da3 100644 --- a/framework/i18n/I18N.php +++ b/framework/i18n/I18N.php @@ -4,91 +4,93 @@ 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; } - if (strpos($message, '|') !== false && preg_match('/^([\w\-\.]+)\|(.*)/', $message, $matches)) { + // 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->getPluralFormat($message, $params[0], $language); -// if (!isset($params['{n}'])) { -// $params['{n}'] = $params[0]; -// } -// unset($params[0]); -// } + $message = $this->getMessageSource($category)->translate($category, $message, $language); - return $params === array() ? $message : strtr($message, $params); - } + if (!is_array($params)) { + $params = array($params); + } - public function getLocale($language) - { + 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) { - return $category === 'yii' ? $this->getMessages() : $this->getCoreMessages(); - } - - private $_coreMessages; - private $_messages; - - public function getCoreMessages() - { - if (is_object($this->_coreMessages)) { - return $this->_coreMessages; - } elseif ($this->_coreMessages === null) { - return $this->_coreMessages = new PhpMessageSource(array( - 'sourceLanguage' => 'en_US', - 'basePath' => '@yii/messages', - )); + if (isset($this->translations[$category])) { + $source = $this->translations[$category]; } else { - return $this->_coreMessages = Yii::createObject($this->_coreMessages); + // try wildcard matching + foreach ($this->translations as $pattern => $config) { + if (substr($pattern, -1) === '*' && strpos($category, rtrim($pattern, '*')) === 0) { + $source = $config; + break; + } + } } - } - - public function setCoreMessages($config) - { - $this->_coreMessages = $config; - } - - public function getMessages() - { - if (is_object($this->_messages)) { - return $this->_messages; - } elseif ($this->_messages === null) { - return $this->_messages = new PhpMessageSource(array( - 'sourceLanguage' => 'en_US', - 'basePath' => '@app/messages', - )); + if (isset($source)) { + return $source instanceof MessageSource ? $source : Yii::createObject($source); } else { - return $this->_messages = Yii::createObject($this->_messages); + throw new InvalidConfigException("Unable to locate message source for category '$category'."); } } - public function setMessages($config) + public function getLocale($language) { - $this->_messages = $config; + } - protected function getPluralFormat($message, $number, $language) + protected function getPluralForm($message, $number, $language) { if (strpos($message, '|') === false) { return $message; @@ -96,7 +98,7 @@ class I18N extends Component $chunks = explode('|', $message); $rules = $this->getLocale($language)->getPluralRules(); foreach ($rules as $i => $rule) { - if (isset($chunks[$i]) && self::evaluate($rule, $number)) { + if (isset($chunks[$i]) && $this->evaluate($rule, $number)) { return $chunks[$i]; } } @@ -110,7 +112,7 @@ class I18N extends Component * @param mixed $n the number value * @return boolean the expression result */ - protected static function evaluate($expression, $n) + protected function evaluate($expression, $n) { return @eval("return $expression;"); } diff --git a/framework/i18n/MessageSource.php b/framework/i18n/MessageSource.php index e1df935..cf23338 100644 --- a/framework/i18n/MessageSource.php +++ b/framework/i18n/MessageSource.php @@ -1,9 +1,7 @@ 'core.php', + * 'ext' => 'extensions.php', + * ) + * ~~~ + */ + public $fileMap; /** * Loads the message translation for the specified language and category. @@ -47,7 +57,14 @@ class PhpMessageSource extends MessageSource */ protected function loadMessages($category, $language) { - $messageFile = Yii::getAlias($this->basePath) . "/$language/$category.php"; + $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)) { @@ -55,7 +72,7 @@ class PhpMessageSource extends MessageSource } return $messages; } else { - Yii::error("Message file not found: $messageFile", __CLASS__); + Yii::error("The message file for category '$category' does not exist: $messageFile", __CLASS__); return array(); } } diff --git a/framework/logging/DbTarget.php b/framework/logging/DbTarget.php index 8f38eb8..364b5a4 100644 --- a/framework/logging/DbTarget.php +++ b/framework/logging/DbTarget.php @@ -1,9 +1,7 @@ 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 7198b3a..b71e1a2 100644 --- a/framework/logging/WebTarget.php +++ b/framework/logging/WebTarget.php @@ -1,7 +1,5 @@ + * @since 2.0 + */ +abstract class WebTestCase extends \PHPUnit_Extensions_SeleniumTestCase +{ +} diff --git a/framework/util/VarDumper.php b/framework/util/VarDumper.php deleted file mode 100644 index 7497a03..0000000 --- a/framework/util/VarDumper.php +++ /dev/null @@ -1,144 +0,0 @@ - - * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008-2011 Yii Software LLC - * @license http://www.yiiframework.com/license/ - */ - -namespace yii\util; - -/** - * VarDumper is intended to replace the buggy PHP function var_dump and print_r. - * It can correctly identify the recursively referenced objects in a complex - * object structure. It also has a recursive depth control to avoid indefinite - * recursive display of some peculiar variables. - * - * VarDumper can be used as follows, - *
- * VarDumper::dump($var);
- * 
- * - * @author Qiang Xue - * @since 2.0 - */ -class VarDumper -{ - private static $_objects; - private static $_output; - private static $_depth; - - /** - * Displays a variable. - * This method achieves the similar functionality as var_dump and print_r - * but is more robust when handling complex objects such as Yii controllers. - * @param mixed $var variable to be dumped - * @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) - { - echo self::dumpAsString($var,$depth,$highlight); - } - - /** - * Dumps a variable in terms of a string. - * This method achieves the similar functionality as var_dump and print_r - * but is more robust when handling complex objects such as Yii controllers. - * @param mixed $var variable to be dumped - * @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 - * @return string the string representation of the variable - */ - 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("/','',$result,1); - } - return self::$_output; - } - - /* - * @param mixed $var variable to be dumped - * @param integer $level depth level - */ - private static function dumpInternal($var,$level) - { - switch(gettype($var)) - { - case 'boolean': - self::$_output.=$var?'true':'false'; - break; - case 'integer': - self::$_output.="$var"; - break; - case 'double': - self::$_output.="$var"; - break; - case 'string': - self::$_output.="'".addslashes($var)."'"; - break; - case 'resource': - self::$_output.='{resource}'; - break; - case 'NULL': - self::$_output.="null"; - break; - case 'unknown type': - 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); - } - 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); - } - self::$_output.="\n".$spaces.')'; - } - break; - } - } -} \ No newline at end of file diff --git a/framework/validators/BooleanValidator.php b/framework/validators/BooleanValidator.php index acf79be..427fa44 100644 --- a/framework/validators/BooleanValidator.php +++ b/framework/validators/BooleanValidator.php @@ -1,9 +1,7 @@ owner; +$context = $this->context; +$title = $context->htmlEncode($exception instanceof \yii\base\Exception || $exception instanceof \yii\base\ErrorException ? $exception->getName() : get_class($exception)); ?> - <?php echo get_class($exception)?> + <?php echo $title?> ", Html::style($content)); + $this->assertEquals("", Html::style($content, array('type' => 'text/less'))); + } + + public function testScript() + { + $content = 'a <>'; + $this->assertEquals("", Html::script($content)); + $this->assertEquals("", Html::script($content, array('type' => 'text/js'))); + } + + public function testCssFile() + { + $this->assertEquals('', Html::cssFile('http://example.com')); + $this->assertEquals('', Html::cssFile('')); + } + + public function testJsFile() + { + $this->assertEquals('', Html::jsFile('http://example.com')); + $this->assertEquals('', Html::jsFile('')); + } + + public function testBeginForm() + { + $this->assertEquals('
', Html::beginForm()); + $this->assertEquals('', Html::beginForm('/example', 'get')); + $hiddens = array( + '', + '', + ); + $this->assertEquals('' . "\n" . implode("\n", $hiddens), Html::beginForm('/example?id=1&title=%3C', 'get')); + } + + public function testEndForm() + { + $this->assertEquals('
', Html::endForm()); + } + + public function testA() + { + $this->assertEquals('something<>', Html::a('something<>')); + $this->assertEquals('something', Html::a('something', '/example')); + $this->assertEquals('something', Html::a('something', '')); + } + + public function testMailto() + { + $this->assertEquals('test<>', Html::mailto('test<>')); + $this->assertEquals('test<>', Html::mailto('test<>', 'test>')); + } + + public function testImg() + { + $this->assertEquals('', Html::img('/example')); + $this->assertEquals('', Html::img('')); + $this->assertEquals('something', Html::img('/example', array('alt' => 'something', 'width' => 10))); + } + + public function testLabel() + { + $this->assertEquals('', Html::label('something<>')); + $this->assertEquals('', Html::label('something<>', 'a')); + $this->assertEquals('', Html::label('something<>', 'a', array('class' => 'test'))); + } + + public function testButton() + { + $this->assertEquals('', Html::button()); + $this->assertEquals('', Html::button('test', 'value', 'content<>')); + $this->assertEquals('', Html::button('test', 'value', 'content<>', array('type' => 'submit', 'class' => "t"))); + } + + public function testSubmitButton() + { + $this->assertEquals('', Html::submitButton()); + $this->assertEquals('', Html::submitButton('test', 'value', 'content<>', array('class' => 't'))); + } + + public function testResetButton() + { + $this->assertEquals('', Html::resetButton()); + $this->assertEquals('', Html::resetButton('test', 'value', 'content<>', array('class' => 't'))); + } + + public function testInput() + { + $this->assertEquals('', Html::input('text')); + $this->assertEquals('', Html::input('text', 'test', 'value', array('class' => 't'))); + } + + public function testButtonInput() + { + $this->assertEquals('', Html::buttonInput('test')); + $this->assertEquals('', Html::buttonInput('test', 'text', array('class' => 'a'))); + } + + public function testSubmitInput() + { + $this->assertEquals('', Html::submitInput()); + $this->assertEquals('', Html::submitInput('test', 'text', array('class' => 'a'))); + } + + public function testResetInput() + { + $this->assertEquals('', Html::resetInput()); + $this->assertEquals('', Html::resetInput('test', 'text', array('class' => 'a'))); + } + + public function testTextInput() + { + $this->assertEquals('', Html::textInput('test')); + $this->assertEquals('', Html::textInput('test', 'value', array('class' => 't'))); + } + + public function testHiddenInput() + { + $this->assertEquals('', Html::hiddenInput('test')); + $this->assertEquals('', Html::hiddenInput('test', 'value', array('class' => 't'))); + } + + public function testPasswordInput() + { + $this->assertEquals('', Html::passwordInput('test')); + $this->assertEquals('', Html::passwordInput('test', 'value', array('class' => 't'))); + } + + public function testFileInput() + { + $this->assertEquals('', Html::fileInput('test')); + $this->assertEquals('', Html::fileInput('test', 'value', array('class' => 't'))); + } + + public function testTextarea() + { + $this->assertEquals('', Html::textarea('test')); + $this->assertEquals('', Html::textarea('test', 'value<>', array('class' => 't'))); + } + + public function testRadio() + { + $this->assertEquals('', Html::radio('test')); + $this->assertEquals('', Html::radio('test', true, null, array('class' => 'a'))); + $this->assertEquals('', Html::radio('test', true, 2, array('class' => 'a' , 'uncheck' => '0'))); + } + + public function testCheckbox() + { + $this->assertEquals('', Html::checkbox('test')); + $this->assertEquals('', Html::checkbox('test', true, null, array('class' => 'a'))); + $this->assertEquals('', Html::checkbox('test', true, 2, array('class' => 'a', 'uncheck' => '0'))); + } + + public function testDropDownList() + { + $expected = << + + +EOD; + $this->assertEquals($expected, Html::dropDownList('test')); + $expected = << + + + +EOD; + $this->assertEquals($expected, Html::dropDownList('test', null, $this->getDataItems())); + $expected = << + + + +EOD; + $this->assertEquals($expected, Html::dropDownList('test', 'value2', $this->getDataItems())); + } + + public function testListBox() + { + $expected = << + + +EOD; + $this->assertEquals($expected, Html::listBox('test')); + $expected = << + + + +EOD; + $this->assertEquals($expected, Html::listBox('test', null, $this->getDataItems(), array('size' => 5))); + $expected = << + + + +EOD; + $this->assertEquals($expected, Html::listBox('test', null, $this->getDataItems2())); + $expected = << + + + +EOD; + $this->assertEquals($expected, Html::listBox('test', 'value2', $this->getDataItems())); + $expected = << + + + +EOD; + $this->assertEquals($expected, Html::listBox('test', array('value1', 'value2'), $this->getDataItems())); + + $expected = << + + +EOD; + $this->assertEquals($expected, Html::listBox('test', null, array(), array('multiple' => true))); + $expected = << +EOD; + $this->assertEquals($expected, Html::listBox('test', '', array(), array('unselect' => '0'))); + } + + public function testCheckboxList() + { + $this->assertEquals('', Html::checkboxList('test')); + + $expected = << text1 + +EOD; + $this->assertEquals($expected, Html::checkboxList('test', array('value2'), $this->getDataItems())); + + $expected = << text1<> + +EOD; + $this->assertEquals($expected, Html::checkboxList('test', array('value2'), $this->getDataItems2())); + + $expected = <<
+ +EOD; + $this->assertEquals($expected, Html::checkboxList('test', array('value2'), $this->getDataItems(), array( + 'separator' => "
\n", + 'unselect' => '0', + ))); + + $expected = <<text1 +1 +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 = << text1 + +EOD; + $this->assertEquals($expected, Html::radioList('test', array('value2'), $this->getDataItems())); + + $expected = << text1<> + +EOD; + $this->assertEquals($expected, Html::radioList('test', array('value2'), $this->getDataItems2())); + + $expected = <<
+ +EOD; + $this->assertEquals($expected, Html::radioList('test', array('value2'), $this->getDataItems(), array( + 'separator' => "
\n", + 'unselect' => '0', + ))); + + $expected = <<text1 +1 +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 = <<please select<> + + + + + + + + + + + + + + +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 @@ +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..fcdcf7d --- /dev/null +++ b/tests/unit/framework/web/UrlManagerTest.php @@ -0,0 +1,201 @@ + '/', + )); + $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/', + )); + $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' => '/', + )); + $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/', + )); + $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', + )); + $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, + 'cacheID' => false, + 'rules' => array( + array( + 'pattern' => 'post//', + '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, + 'cacheID' => false, + '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', + )); + $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; + $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, + )); + // 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, + 'cacheID' => false, + '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', + 'cacheID' => false, + '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..8b2b578 --- /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; + $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; + $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 7249235..f66d3c1 100644 --- a/todo.md +++ b/todo.md @@ -1,32 +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 - * backend-specific 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 @@ -34,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 @@ -52,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.