diff --git a/app/assets/.gitignore b/app/assets/.gitignore new file mode 100644 index 0000000..72e8ffc --- /dev/null +++ b/app/assets/.gitignore @@ -0,0 +1 @@ +* diff --git a/app/index.php b/app/index.php new file mode 100644 index 0000000..8f98090 --- /dev/null +++ b/app/index.php @@ -0,0 +1,9 @@ +run(); diff --git a/app/protected/config/main.php b/app/protected/config/main.php new file mode 100644 index 0000000..e18ead8 --- /dev/null +++ b/app/protected/config/main.php @@ -0,0 +1,15 @@ + 'hello', + 'basePath' => dirname(__DIR__), + 'components' => array( + 'cache' => array( + 'class' => 'yii\caching\FileCache', + ), + 'user' => array( + 'class' => 'yii\web\User', + 'identityClass' => 'app\models\User', + ) + ), +); \ No newline at end of file diff --git a/app/protected/controllers/SiteController.php b/app/protected/controllers/SiteController.php new file mode 100644 index 0000000..58e9568 --- /dev/null +++ b/app/protected/controllers/SiteController.php @@ -0,0 +1,22 @@ +render('index'); + } + + public function actionLogin() + { + $user = app\models\User::findIdentity(100); + Yii::$app->getUser()->login($user); + Yii::$app->getResponse()->redirect(array('site/index')); + } + + public function actionLogout() + { + Yii::$app->getUser()->logout(); + Yii::$app->getResponse()->redirect(array('site/index')); + } +} \ No newline at end of file diff --git a/app/protected/models/User.php b/app/protected/models/User.php new file mode 100644 index 0000000..cebf1da --- /dev/null +++ b/app/protected/models/User.php @@ -0,0 +1,43 @@ + array( + 'id' => '100', + 'authKey' => 'test100key', + 'name' => 'admin', + ), + '101' => array( + 'id' => '101', + 'authKey' => 'test101key', + 'name' => 'demo', + ), + ); + + public static function findIdentity($id) + { + return isset(self::$users[$id]) ? new self(self::$users[$id]) : null; + } + + public function getId() + { + return $this->id; + } + + public function getAuthKey() + { + return $this->authKey; + } + + public function validateAuthKey($authKey) + { + return $this->authKey === $authKey; + } +} \ No newline at end of file diff --git a/app/protected/runtime/.gitignore b/app/protected/runtime/.gitignore new file mode 100644 index 0000000..72e8ffc --- /dev/null +++ b/app/protected/runtime/.gitignore @@ -0,0 +1 @@ +* diff --git a/app/protected/views/layouts/main.php b/app/protected/views/layouts/main.php new file mode 100644 index 0000000..092e665 --- /dev/null +++ b/app/protected/views/layouts/main.php @@ -0,0 +1,22 @@ + + + +beginPage(); ?> + + <?php echo Html::encode($this->title); ?> + head(); ?> + + +

Welcome

+beginBody(); ?> + +endBody(); ?> + +endPage(); ?> + diff --git a/app/protected/views/site/index.php b/app/protected/views/site/index.php new file mode 100644 index 0000000..3b83080 --- /dev/null +++ b/app/protected/views/site/index.php @@ -0,0 +1,17 @@ +title = 'Hello World'; + +$user = Yii::$app->getUser(); +if ($user->isGuest) { + echo Html::a('login', array('login')); +} else { + echo "You are logged in as " . $user->identity->name . "
"; + echo Html::a('logout', array('logout')); +} +?> + + diff --git a/framework/YiiBase.php b/framework/YiiBase.php index 6df998b..9d501b1 100644 --- a/framework/YiiBase.php +++ b/framework/YiiBase.php @@ -7,6 +7,7 @@ use yii\base\Exception; use yii\base\InvalidConfigException; use yii\base\InvalidParamException; +use yii\base\UnknownClassException; use yii\logging\Logger; /** @@ -54,13 +55,10 @@ class YiiBase */ public static $classMap = array(); /** - * @var array list of directories where Yii will search for new classes to be included. - * The first directory in the array will be searched first, and so on. - * This property mainly affects how [[autoload]] works. - * @see import - * @see autoload + * @var boolean whether to search PHP include_path when autoloading unknown classes. + * You may want to turn this off if you are also using autoloaders from other libraries. */ - public static $classPath = array(); + public static $enableIncludePath = true; /** * @var yii\console\Application|yii\web\Application the application instance */ @@ -107,88 +105,86 @@ class YiiBase } /** - * Imports a class or a directory. - * - * Importing a class is like including the corresponding class file. - * The main difference is that importing a class is much lighter because it only - * includes the class file when the class is referenced in the code the first time. - * - * Importing a directory will add the directory to the front of the [[classPath]] array. - * When [[autoload()]] is loading an unknown class, it will search in the directories - * specified in [[classPath]] to find the corresponding class file to include. - * For this reason, if multiple directories are imported, the directories imported later - * will take precedence in class file searching. - * - * The same class or directory can be imported multiple times. Only the first importing - * will count. Importing a directory does not import any of its subdirectories. - * - * To import a class or a directory, one can use either path alias or class name (can be namespaced): - * - * - `@app/components/GoogleMap`: importing the `GoogleMap` class with a path alias; - * - `@app/components/*`: importing the whole `components` directory with a path alias; - * - `GoogleMap`: importing the `GoogleMap` class with a class name. [[autoload()]] will be used - * when this class is used for the first time. - * - * @param string $alias path alias or a simple class name to be imported - * @param boolean $forceInclude whether to include the class file immediately. If false, the class file - * will be included only when the class is being used. This parameter is used only when - * the path alias refers to a class. - * @return string the class name or the directory that this alias refers to - * @throws Exception if the path alias is invalid + * Imports a class by its alias. + * + * This method is provided to support autoloading of non-namespaced classes. + * Such a class can be specified in terms of an alias. For example, the alias `@old/code/Sample` + * may represent the `Sample` class under the directory `@old/code` (a path alias). + * + * By importing a class, the class is put in an internal storage such that when + * the class is used for the first time, the class autoloader will be able to + * find the corresponding class file and include it. For this reason, this method + * is much lighter than `include()`. + * + * You may import the same class multiple times. Only the first importing will count. + * + * @param string $alias the class to be imported. This may be either a class alias or a fully-qualified class name. + * If the latter, it will be returned back without change. + * @return string the actual class name that `$alias` refers to + * @throws Exception if the alias is invalid */ - public static function import($alias, $forceInclude = false) + public static function import($alias) { - if (isset(self::$_imported[$alias])) { - return self::$_imported[$alias]; - } - - if ($alias[0] !== '@') { - // a simple class name - if (class_exists($alias, false) || interface_exists($alias, false)) { - return self::$_imported[$alias] = $alias; - } - if ($forceInclude && static::autoload($alias)) { - self::$_imported[$alias] = $alias; - } + if (strncmp($alias, '@', 1)) { return $alias; + } else { + $alias = static::getAlias($alias); + if (!isset(self::$_imported[$alias])) { + $className = basename($alias); + self::$_imported[$alias] = $className; + self::$classMap[$className] = $alias . '.php'; + } + return self::$_imported[$alias]; } + } - $className = basename($alias); - $isClass = $className !== '*'; - - if ($isClass && (class_exists($className, false) || interface_exists($className, false))) { - return self::$_imported[$alias] = $className; - } - - $path = static::getAlias(dirname($alias)); - - if ($isClass) { - if ($forceInclude) { - require($path . "/$className.php"); - self::$_imported[$alias] = $className; - } else { - self::$classMap[$className] = $path . DIRECTORY_SEPARATOR . "$className.php"; + /** + * Imports a set of namespaces. + * + * By importing a namespace, the method will create an alias for the directory corresponding + * to the namespace. For example, if "foo\bar" is a namespace associated with the directory + * "path/to/foo/bar", then an alias "@foo/bar" will be created for this directory. + * + * This method is typically invoked in the bootstrap file to import the namespaces of + * the installed extensions. By default, Composer, when installing new extensions, will + * generate such a mapping file which can be loaded and passed to this method. + * + * @param array $namespaces the namespaces to be imported. The keys are the namespaces, + * and the values are the corresponding directories. + */ + public static function importNamespaces($namespaces) + { + foreach ($namespaces as $name => $path) { + if ($name !== '') { + $name = '@' . str_replace('\\', '/', $name); + static::setAlias($name, $path); } - return $className; - } else { - // a directory - array_unshift(self::$classPath, $path); - return self::$_imported[$alias] = $path; } } /** * Translates a path alias into an actual path. * - * The path alias can be either a root alias registered via [[setAlias]] or an - * alias starting with a root alias (e.g. `@yii/base/Component.php`). - * In the latter case, the root alias will be replaced by the corresponding registered path - * and the remaining part will be appended to it. + * The translation is done according to the following procedure: + * + * 1. If the given alias does not start with '@', it is returned back without change; + * 2. Otherwise, look for the longest registered alias that matches the beginning part + * of the given alias. If it exists, replace the matching part of the given alias with + * the corresponding registered path. + * 3. Throw an exception or return false, depending on the `$throwException` parameter. * - * In case the given parameter is not an alias (i.e., not starting with '@'), - * it will be returned back without change. + * For example, by default '@yii' is registered as the alias to the Yii framework directory, + * say '/path/to/yii'. The alias '@yii/web' would then be translated into '/path/to/yii/web'. + * + * If you have registered two aliases '@foo' and '@foo/bar'. Then translating '@foo/bar/config' + * would replace the part '@foo/bar' (instead of '@foo') with the corresponding registered path. + * This is because the longest alias takes precedence. + * + * However, if the alias to be translated is '@foo/barbar/config', then '@foo' will be replaced + * instead of '@foo/bar', because '/' serves as the boundary character. + * + * Note, this method does not check if the returned path exists or not. * - * Note, this method does not ensure the existence of the resulting path. * @param string $alias the alias to be translated. * @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. @@ -198,18 +194,26 @@ class YiiBase */ public static function getAlias($alias, $throwException = true) { - 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); + if (strncmp($alias, '@', 1)) { + // not an alias + return $alias; + } + + $pos = strpos($alias, '/'); + $root = $pos === false ? $alias : substr($alias, 0, $pos); + + if (isset(self::$aliases[$root])) { + if (is_string(self::$aliases[$root])) { + return $pos === false ? self::$aliases[$root] : self::$aliases[$root] . substr($alias, $pos); + } else { + foreach (self::$aliases[$root] as $name => $path) { + if (strpos($alias . '/', $name . '/') === 0) { + return $path . substr($alias, strlen($name)); + } } } } + if ($throwException) { throw new InvalidParamException("Invalid path alias: $alias"); } else { @@ -218,41 +222,96 @@ class YiiBase } /** + * Returns the root alias part of a given alias. + * A root alias is an alias that has been registered via [[setAlias()]] previously. + * If a given alias matches multiple root aliases, the longest one will be returned. + * @param string $alias the alias + * @return string|boolean the root alias, or false if no root alias is found + */ + public static function getRootAlias($alias) + { + $pos = strpos($alias, '/'); + $root = $pos === false ? $alias : substr($alias, 0, $pos); + + if (isset(self::$aliases[$root])) { + if (is_string(self::$aliases[$root])) { + return $root; + } else { + foreach (self::$aliases[$root] as $name => $path) { + if (strpos($alias . '/', $name . '/') === 0) { + return $name; + } + } + } + } + return false; + } + + /** * Registers a path alias. * - * A path alias is a short name representing a path (a file path, a URL, etc.) - * A path alias must start with '@' (e.g. '@yii'). + * A path alias is a short name representing a long path (a file path, a URL, etc.) + * For example, we use '@yii' as the alias of the path to the Yii framework directory. + * + * A path alias must start with the character '@' so that it can be easily differentiated + * from non-alias paths. + * + * Note that this method does not check if the given path exists or not. All it does is + * to associate the alias with the path. * - * Note that this method neither checks the existence of the path nor normalizes the path. - * Any trailing '/' and '\' characters in the path will be trimmed. + * Any trailing '/' and '\' characters in the given path will be trimmed. * - * @param string $alias the alias name (e.g. "@yii"). It should start with a '@' character - * and should NOT contain the forward slash "/" or the backward slash "\". - * @param string $path the path corresponding to the alias. This can be + * @param string $alias the alias name (e.g. "@yii"). It must start with a '@' character. + * It may contain the forward slash '/' which serves as boundary character when performing + * alias translation by [[getAlias()]]. + * @param string $path the path corresponding to the alias. Trailing '/' and '\' characters + * will be trimmed. This can be * * - a directory or a file path (e.g. `/tmp`, `/tmp/main.txt`) * - a URL (e.g. `http://www.yiiframework.com`) * - a path alias (e.g. `@yii/base`). In this case, the path alias will be converted into the * actual path first by calling [[getAlias()]]. * - * @throws Exception if $path is an invalid alias + * @throws InvalidParamException if $path is an invalid alias. * @see getAlias */ public static function setAlias($alias, $path) { - if ($path === null) { - unset(self::$aliases[$alias]); - } elseif (strncmp($path, '@', 1)) { - self::$aliases[$alias] = rtrim($path, '\\/'); - } else { - self::$aliases[$alias] = static::getAlias($path); + if (strncmp($alias, '@', 1)) { + $alias = '@' . $alias; + } + $pos = strpos($alias, '/'); + $root = $pos === false ? $alias : substr($alias, 0, $pos); + if ($path !== null) { + $path = strncmp($path, '@', 1) ? rtrim($path, '\\/') : static::getAlias($path); + if (!isset(self::$aliases[$root])) { + self::$aliases[$root] = $path; + } elseif (is_string(self::$aliases[$root])) { + if ($pos === false) { + self::$aliases[$root] = $path; + } else { + self::$aliases[$root] = array( + $alias => $path, + $root => self::$aliases[$root], + ); + } + } else { + self::$aliases[$root][$alias] = $path; + krsort(self::$aliases[$root]); + } + } elseif (isset(self::$aliases[$root])) { + if (is_array(self::$aliases[$root])) { + unset(self::$aliases[$root][$alias]); + } elseif ($pos === false) { + unset(self::$aliases[$root]); + } } } /** * Class autoload loader. - * This method is invoked automatically when the execution encounters an unknown class. - * The method will attempt to include the class file as follows: + * This method is invoked automatically when PHP sees an unknown class. + * The method will attempt to include the class file according to the following procedure: * * 1. Search in [[classMap]]; * 2. If the class is namespaced (e.g. `yii\base\Component`), it will attempt @@ -261,43 +320,58 @@ class YiiBase * 3. If the class is named in PEAR style (e.g. `PHPUnit_Framework_TestCase`), * it will attempt to include the file associated with the corresponding path alias * (e.g. `@PHPUnit/Framework/TestCase.php`); - * 4. Search in [[classPath]]; + * 4. Search PHP include_path for the actual class file if [[enableIncludePath]] is true; * 5. Return false so that other autoloaders have chance to include the class file. * * @param string $className class name * @return boolean whether the class has been loaded successfully - * @throws Exception if the class file does not exist + * @throws InvalidConfigException if the class file does not exist + * @throws UnknownClassException if the class does not exist in the class file */ public static function autoload($className) { $className = ltrim($className, '\\'); if (isset(self::$classMap[$className])) { - $classFile = self::$classMap[$className]; + $classFile = static::getAlias(self::$classMap[$className]); + if (!is_file($classFile)) { + throw new InvalidConfigException("Class file does not exist: $classFile"); + } } else { + // follow PSR-0 to determine the class file if (($pos = strrpos($className, '\\')) !== false) { // namespaced class, e.g. yii\base\Component - $classFile = str_replace('\\', '/', substr($className, 0, $pos + 1)) + $path = str_replace('\\', '/', substr($className, 0, $pos + 1)) . str_replace('_', '/', substr($className, $pos + 1)) . '.php'; } else { - $classFile = str_replace('_', '/', $className) . '.php'; + $path = str_replace('_', '/', $className) . '.php'; } - if (strpos($classFile, '/') !== false) { - // make it into a path alias - $classFile = '@' . $classFile; + + // try via path alias first + if (strpos($path, '/') !== false) { + $fullPath = static::getAlias('@' . $path, false); + if ($fullPath !== false && is_file($fullPath)) { + $classFile = $fullPath; + } } - } - $classFile = static::getAlias($classFile); - if ($classFile !== false && is_file($classFile)) { - include($classFile); - if (class_exists($className, false) || interface_exists($className, false)) { - return true; - } else { - throw new Exception("Unable to find '$className' in file: $classFile"); + // search include_path + if (!isset($classFile) && self::$enableIncludePath && ($fullPath = stream_resolve_include_path($path)) !== false) { + $classFile = $fullPath; + } + + if (!isset($classFile)) { + // return false to let other autoloaders to try loading the class + return false; } + } + + include($classFile); + + if (class_exists($className, false) || interface_exists($className, false)) { + return true; } else { - return false; + throw new UnknownClassException("Unable to find '$className' in file: $classFile"); } } @@ -305,16 +379,16 @@ class YiiBase * Creates a new object using the given configuration. * * The configuration can be either a string or an array. - * If a string, it is treated as the *object type*; if an array, - * it must contain a `class` element specifying the *object type*, and + * If a string, it is treated as the *object class*; if an array, + * it must contain a `class` element specifying the *object class*, and * the rest of the name-value pairs in the array will be used to initialize * the corresponding object properties. * - * The object type can be either a class name or the [[getAlias|alias]] of + * The object type can be either a class name or the [[getAlias()|alias]] of * the class. For example, * - * - `\app\components\GoogleMap`: fully-qualified namespaced class. - * - `@app/components/GoogleMap`: an alias + * - `app\components\GoogleMap`: fully-qualified namespaced class. + * - `@app/components/GoogleMap`: an alias, used for non-namespaced class. * * Below are some usage examples: * @@ -357,7 +431,7 @@ class YiiBase } if (!class_exists($class, false)) { - $class = static::import($class, true); + $class = static::import($class); } $class = ltrim($class, '\\'); diff --git a/framework/assets.php b/framework/assets.php new file mode 100644 index 0000000..5efa94a --- /dev/null +++ b/framework/assets.php @@ -0,0 +1,5 @@ + __DIR__ . '/web/assets', +); \ No newline at end of file diff --git a/framework/base/Application.php b/framework/base/Application.php index e1c1d60..c498a8e 100644 --- a/framework/base/Application.php +++ b/framework/base/Application.php @@ -13,36 +13,6 @@ use yii\helpers\FileHelper; /** * Application is the base class for all application classes. * - * An application serves as the global context that the user request - * is being processed. It manages a set of application components that - * provide specific functionalities to the whole application. - * - * The core application components provided by Application are the following: - * - * - * Application will undergo the following life cycles when processing a user request: - *
    - *
  1. load application configuration;
  2. - *
  3. set up class autoloader and error handling;
  4. - *
  5. load static application components;
  6. - *
  7. {@link beforeRequest}: preprocess the user request; `beforeRequest` event raised.
  8. - *
  9. {@link processRequest}: process the user request;
  10. - *
  11. {@link afterRequest}: postprocess the user request; `afterRequest` event raised.
  12. - *
- * - * Starting from lifecycle 3, if a PHP error or an uncaught exception occurs, - * the application will switch to its error handling logic and jump to step 6 afterwards. - * * @author Qiang Xue * @since 2.0 */ @@ -87,12 +57,10 @@ class Application extends Module */ public $layout = 'main'; - private $_runtimePath; private $_ended = false; /** - * @var string Used to reserve memory for fatal error handler. This memory - * reserve can be removed if it's OK to write to PHP log only in this particular case. + * @var string Used to reserve memory for fatal error handler. */ private $_memoryReserve; @@ -112,8 +80,8 @@ class Application extends Module if (isset($config['basePath'])) { $this->setBasePath($config['basePath']); + Yii::setAlias('@app', $this->getBasePath()); unset($config['basePath']); - Yii::$aliases['@app'] = $this->getBasePath(); } else { throw new InvalidConfigException('The "basePath" configuration is required.'); } @@ -158,30 +126,6 @@ class Application extends Module } /** - * 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']); - $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) @@ -224,6 +168,8 @@ class Application extends Module return 0; } + private $_runtimePath; + /** * Returns the directory that stores runtime files. * @return string the directory that stores runtime files. Defaults to 'protected/runtime'. @@ -243,12 +189,35 @@ class Application extends Module */ public function setRuntimePath($path) { - $p = FileHelper::ensureDirectory($path); - if (is_writable($p)) { - $this->_runtimePath = $p; + $path = Yii::getAlias($path); + if (is_dir($path) && is_writable($path)) { + $this->_runtimePath = $path; } else { - throw new InvalidConfigException("Runtime path must be writable by the Web server process: $path"); + throw new InvalidConfigException("Runtime path must be a directory writable by the Web server process: $path"); + } + } + + private $_vendorPath; + + /** + * Returns the directory that stores vendor files. + * @return string the directory that stores vendor files. Defaults to 'protected/vendor'. + */ + public function getVendorPath() + { + if ($this->_vendorPath === null) { + $this->setVendorPath($this->getBasePath() . DIRECTORY_SEPARATOR . 'vendor'); } + return $this->_vendorPath; + } + + /** + * Sets the directory that stores vendor files. + * @param string $path the directory that stores vendor files. + */ + public function setVendorPath($path) + { + $this->_vendorPath = Yii::getAlias($path); } /** @@ -359,6 +328,45 @@ class Application extends Module } /** + * Handles uncaught PHP exceptions. + * + * This method is implemented as a PHP exception handler. It requires + * that constant YII_ENABLE_ERROR_HANDLER be defined true. + * + * @param \Exception $exception exception that is not caught + */ + public function handleException($exception) + { + // disable error capturing to avoid recursive errors while handling exceptions + restore_error_handler(); + restore_exception_handler(); + + try { + $this->logException($exception); + + if (($handler = $this->getErrorHandler()) !== null) { + $handler->handle($exception); + } else { + $this->renderException($exception); + } + + $this->end(1); + + } 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); + } + } + + /** * Handles PHP execution errors such as warnings, notices. * * This method is used as a PHP error handler. It will simply raise an `ErrorException`. @@ -389,41 +397,27 @@ class Application extends Module } /** - * Handles uncaught PHP exceptions. - * - * This method is implemented as a PHP exception handler. It requires - * that constant YII_ENABLE_ERROR_HANDLER be defined true. - * - * @param \Exception $exception exception that is not caught + * Handles fatal PHP errors */ - public function handleException($exception) + public function handleFatalError() { - // disable error capturing to avoid recursive errors while handling exceptions - restore_error_handler(); - restore_exception_handler(); - - try { - $this->logException($exception); + if (YII_ENABLE_ERROR_HANDLER) { + $error = error_get_last(); - if (($handler = $this->getErrorHandler()) !== null) { - $handler->handle($exception); - } else { - $this->renderException($exception); - } + if (ErrorException::isFatalError($error)) { + unset($this->_memoryReserve); + $exception = new ErrorException($error['message'], $error['type'], $error['type'], $error['file'], $error['line']); + // use error_log because it's too late to use Yii log + error_log($exception); - $this->end(1); + if (($handler = $this->getErrorHandler()) !== null) { + $handler->handle($exception); + } else { + $this->renderException($exception); + } - } 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; + exit(1); } - $msg .= "\n\$_SERVER = " . var_export($_SERVER, true); - error_log($msg); - exit(1); } } diff --git a/framework/base/Controller.php b/framework/base/Controller.php index 6ff68da..d11d19b 100644 --- a/framework/base/Controller.php +++ b/framework/base/Controller.php @@ -428,7 +428,7 @@ class Controller extends Component $file = $this->getViewPath() . DIRECTORY_SEPARATOR . $view; } - return FileHelper::getExtension($file) === '' ? $file . '.php' : $file; + return pathinfo($file, PATHINFO_EXTENSION) === '' ? $file . '.php' : $file; } /** @@ -463,7 +463,7 @@ class Controller extends Component $file = $module->getLayoutPath() . DIRECTORY_SEPARATOR . $view; } - if (FileHelper::getExtension($file) === '') { + if (pathinfo($file, PATHINFO_EXTENSION) === '') { $file .= '.php'; } return $file; diff --git a/framework/base/ErrorException.php b/framework/base/ErrorException.php index 93390e7..b41e9ed 100644 --- a/framework/base/ErrorException.php +++ b/framework/base/ErrorException.php @@ -79,7 +79,7 @@ class ErrorException extends Exception * @param array $error error got from error_get_last() * @return bool if error is one of fatal type */ - public static function isFatalErorr($error) + public static function isFatalError($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)); } diff --git a/framework/base/ErrorHandler.php b/framework/base/ErrorHandler.php index a2f372c..98a061d 100644 --- a/framework/base/ErrorHandler.php +++ b/framework/base/ErrorHandler.php @@ -51,6 +51,7 @@ class ErrorHandler extends Component /** + * Handles exception * @param \Exception $exception */ public function handle($exception) @@ -64,6 +65,10 @@ class ErrorHandler extends Component $this->renderException($exception); } + /** + * Renders exception + * @param \Exception $exception + */ protected function renderException($exception) { if ($this->errorAction !== null) { @@ -76,6 +81,12 @@ class ErrorHandler extends Component if (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] === 'XMLHttpRequest') { \Yii::$app->renderException($exception); } else { + // if there is an error during error rendering it's useful to + // display PHP error in debug mode instead of a blank screen + if(YII_DEBUG) { + ini_set('display_errors', 1); + } + $view = new View; if (!YII_DEBUG || $exception instanceof UserException) { $viewName = $this->errorView; @@ -196,6 +207,10 @@ class ErrorHandler extends Component echo '
' . $output . '
'; } + /** + * Renders calls stack trace + * @param array $trace + */ public function renderTrace($trace) { $count = 0; @@ -233,6 +248,11 @@ class ErrorHandler extends Component echo ''; } + /** + * Converts special characters to HTML entities + * @param string $text text to encode + * @return string + */ public function htmlEncode($text) { return htmlspecialchars($text, ENT_QUOTES, \Yii::$app->charset); diff --git a/framework/base/Module.php b/framework/base/Module.php index 2ccf61d..d99778d 100644 --- a/framework/base/Module.php +++ b/framework/base/Module.php @@ -207,11 +207,17 @@ abstract class Module extends Component * Sets the root directory of the module. * This method can only be invoked at the beginning of the constructor. * @param string $path the root directory of the module. This can be either a directory name or a path alias. - * @throws Exception if the directory does not exist. + * @throws InvalidParamException if the directory does not exist. */ public function setBasePath($path) { - $this->_basePath = FileHelper::ensureDirectory($path); + $path = Yii::getAlias($path); + $p = realpath($path); + if ($p !== false && is_dir($p)) { + $this->_basePath = $p; + } else { + throw new InvalidParamException("The directory does not exist: $path"); + } } /** @@ -236,7 +242,7 @@ abstract class Module extends Component */ public function setControllerPath($path) { - $this->_controllerPath = FileHelper::ensureDirectory($path); + $this->_controllerPath = Yii::getAlias($path); } /** @@ -259,7 +265,7 @@ abstract class Module extends Component */ public function setViewPath($path) { - $this->_viewPath = FileHelper::ensureDirectory($path); + $this->_viewPath = Yii::getAlias($path); } /** @@ -282,20 +288,7 @@ abstract class Module extends Component */ public function setLayoutPath($path) { - $this->_layoutPath = FileHelper::ensureDirectory($path); - } - - /** - * Imports the specified path aliases. - * This method is provided so that you can import a set of path aliases when configuring a module. - * The path aliases will be imported by calling [[Yii::import()]]. - * @param array $aliases list of path aliases to be imported - */ - public function setImport($aliases) - { - foreach ($aliases as $alias) { - Yii::import($alias); - } + $this->_layoutPath = Yii::getAlias($path); } /** @@ -606,14 +599,15 @@ abstract class Module extends Component } elseif (preg_match('/^[a-z0-9\\-_]+$/', $id)) { $className = StringHelper::id2camel($id) . 'Controller'; $classFile = $this->controllerPath . DIRECTORY_SEPARATOR . $className . '.php'; + if (!is_file($classFile)) { + return false; + } $className = ltrim($this->controllerNamespace . '\\' . $className, '\\'); Yii::$classMap[$className] = $classFile; - if (class_exists($className)) { - if (is_subclass_of($className, 'yii\base\Controller')) { - $controller = new $className($id, $this); - } elseif (YII_DEBUG && !is_subclass_of($className, 'yii\base\Controller')) { - throw new InvalidConfigException("Controller class must extend from \\yii\\base\\Controller."); - } + if (is_subclass_of($className, 'yii\base\Controller')) { + $controller = new $className($id, $this); + } elseif (YII_DEBUG) { + throw new InvalidConfigException("Controller class must extend from \\yii\\base\\Controller."); } } diff --git a/framework/base/Response.php b/framework/base/Response.php index a53fd61..396b073 100644 --- a/framework/base/Response.php +++ b/framework/base/Response.php @@ -13,28 +13,38 @@ namespace yii\base; */ class Response extends Component { + /** + * Starts output buffering + */ public function beginOutput() { ob_start(); ob_implicit_flush(false); } + /** + * Returns contents of the output buffer and discards it + * @return string output buffer contents + */ public function endOutput() { return ob_get_clean(); } + /** + * Returns contents of the output buffer + * @return string output buffer contents + */ public function getOutput() { return ob_get_contents(); } - public function cleanOutput() - { - ob_clean(); - } - - public function removeOutput($all = true) + /** + * Discards the output buffer + * @param boolean $all if true recursively discards all output buffers used + */ + public function cleanOutput($all = true) { if ($all) { for ($level = ob_get_level(); $level > 0; --$level) { diff --git a/framework/base/Theme.php b/framework/base/Theme.php index e529a63..a60d56e 100644 --- a/framework/base/Theme.php +++ b/framework/base/Theme.php @@ -61,10 +61,10 @@ class Theme extends Component parent::init(); if (empty($this->pathMap)) { if ($this->basePath !== null) { - $this->basePath = FileHelper::ensureDirectory($this->basePath); + $this->basePath = Yii::getAlias($this->basePath); $this->pathMap = array(Yii::$app->getBasePath() => $this->basePath); } else { - throw new InvalidConfigException("Theme::basePath must be set."); + throw new InvalidConfigException('The "basePath" property must be set.'); } } $paths = array(); @@ -75,7 +75,7 @@ class Theme extends Component } $this->pathMap = $paths; if ($this->baseUrl === null) { - throw new InvalidConfigException("Theme::baseUrl must be set."); + throw new InvalidConfigException('The "baseUrl" property must be set.'); } else { $this->baseUrl = rtrim(Yii::getAlias($this->baseUrl), '/'); } diff --git a/framework/base/UnknownClassException.php b/framework/base/UnknownClassException.php new file mode 100644 index 0000000..ac44746 --- /dev/null +++ b/framework/base/UnknownClassException.php @@ -0,0 +1,26 @@ + + * @since 2.0 + */ +class UnknownClassException extends Exception +{ + /** + * @return string the user-friendly name of this exception + */ + public function getName() + { + return \Yii::t('yii|Unknown Class'); + } +} + diff --git a/framework/base/UnknownMethodException.php b/framework/base/UnknownMethodException.php index 29bedca..440e76e 100644 --- a/framework/base/UnknownMethodException.php +++ b/framework/base/UnknownMethodException.php @@ -8,7 +8,7 @@ namespace yii\base; /** - * UnknownMethodException represents an exception caused by accessing unknown object methods. + * UnknownMethodException represents an exception caused by accessing an unknown object method. * * @author Qiang Xue * @since 2.0 diff --git a/framework/base/View.php b/framework/base/View.php index 8b1f4ef..10a7053 100644 --- a/framework/base/View.php +++ b/framework/base/View.php @@ -10,6 +10,7 @@ namespace yii\base; use Yii; use yii\base\Application; use yii\helpers\FileHelper; +use yii\helpers\Html; /** * View represents a view object in the MVC pattern. @@ -22,22 +23,48 @@ use yii\helpers\FileHelper; class View extends Component { /** - * @event Event an event that is triggered by [[renderFile()]] right before it renders a view file. + * @event ViewEvent an event that is triggered by [[renderFile()]] right before it renders a view file. */ const EVENT_BEFORE_RENDER = 'beforeRender'; /** - * @event Event an event that is triggered by [[renderFile()]] right after it renders a view file. + * @event ViewEvent an event that is triggered by [[renderFile()]] right after it renders a view file. */ const EVENT_AFTER_RENDER = 'afterRender'; /** - * @var object the object that owns this view. This can be a controller, a widget, or any other object. + * The location of registered JavaScript code block or files. + * This means the location is in the head section. */ - public $context; + const POS_HEAD = 1; + /** + * The location of registered JavaScript code block or files. + * This means the location is at the beginning of the body section. + */ + const POS_BEGIN = 2; + /** + * The location of registered JavaScript code block or files. + * This means the location is at the end of the body section. + */ + const POS_END = 3; + /** + * This is internally used as the placeholder for receiving the content registered for the head section. + */ + const PL_HEAD = ''; + /** + * This is internally used as the placeholder for receiving the content registered for the beginning of the body section. + */ + const PL_BODY_BEGIN = ''; /** - * @var ViewContent + * This is internally used as the placeholder for receiving the content registered for the end of the body section. */ - public $content; + const PL_BODY_END = ''; + + + /** + * @var object the context under which the [[renderFile()]] method is being invoked. + * This can be a controller, a widget, or any other object. + */ + public $context; /** * @var mixed custom parameters that are shared among view templates. */ @@ -48,31 +75,75 @@ class View extends Component */ public $renderer; /** - * @var Theme|array the theme object or the configuration array for creating the theme. + * @var Theme|array the theme object or the configuration array for creating the theme object. * If not set, it means theming is not enabled. */ public $theme; /** - * @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 + * @var array a list of named output blocks. The keys are the block names and the values + * are the corresponding block content. You can call [[beginBlock()]] and [[endBlock()]] + * to capture small fragments of a view. They can be later accessed somewhere else * through this property. */ - public $clips; + public $blocks; /** * @var Widget[] the widgets that are currently being rendered (not ended). This property - * is maintained by [[beginWidget()]] and [[endWidget()]] methods. Do not modify it. + * is maintained by [[beginWidget()]] and [[endWidget()]] methods. Do not modify it directly. + * @internal */ public $widgetStack = array(); /** * @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. + * is used internally to implement the content caching feature. Do not modify it directly. + * @internal */ public $cacheStack = array(); /** * @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. + * is used internally to implement the content caching feature. Do not modify it directly. + * @internal */ public $dynamicPlaceholders = array(); + /** + * @var array the registered asset bundles. The keys are the bundle names, and the values + * are the corresponding [[AssetBundle]] objects. + * @see registerAssetBundle + */ + public $assetBundles; + /** + * @var string the page title + */ + public $title; + /** + * @var array the registered meta tags. + * @see registerMetaTag + */ + public $metaTags; + /** + * @var array the registered link tags. + * @see registerLinkTag + */ + public $linkTags; + /** + * @var array the registered CSS code blocks. + * @see registerCss + */ + public $css; + /** + * @var array the registered CSS files. + * @see registerCssFile + */ + public $cssFiles; + /** + * @var array the registered JS code blocks + * @see registerJs + */ + public $js; + /** + * @var array the registered JS files. + * @see registerJsFile + */ + public $jsFiles; /** @@ -87,11 +158,6 @@ class View extends Component if (is_array($this->theme)) { $this->theme = Yii::createObject($this->theme); } - if (is_array($this->content)) { - $this->content = Yii::createObject($this->content); - } else { - $this->content = new ViewContent; - } } /** @@ -165,7 +231,6 @@ class View extends Component } else { $output = $this->renderPhpFile($viewFile, $params); } - $output = $this->content->populate($output); $this->afterRender($viewFile, $output); } @@ -241,7 +306,7 @@ class View extends Component { if (!empty($this->cacheStack)) { $n = count($this->dynamicPlaceholders); - $placeholder = ""; + $placeholder = ""; $this->addDynamicPlaceholder($placeholder, $statements); return $placeholder; } else { @@ -350,26 +415,25 @@ class View extends Component } /** - * 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 + * Begins recording a block. + * This method is a shortcut to beginning [[yii\widgets\Block]] + * @param string $id the block ID. + * @param boolean $renderInPlace whether to render the block content in place. + * Defaults to false, meaning the captured block will not be displayed. + * @return \yii\widgets\Block the Block widget instance */ - public function beginClip($id, $renderInPlace = false) + public function beginBlock($id, $renderInPlace = false) { - return $this->beginWidget('yii\widgets\Clip', array( + return $this->beginWidget('yii\widgets\Block', array( 'id' => $id, 'renderInPlace' => $renderInPlace, )); } /** - * Ends recording a clip. + * Ends recording a block. */ - public function endClip() + public function endBlock() { $this->endWidget(); } @@ -446,4 +510,273 @@ class View extends Component { $this->endWidget(); } + + + private $_assetManager; + + /** + * Registers the asset manager being used by this view object. + * @return \yii\web\AssetManager the asset manager. Defaults to the "assetManager" application component. + */ + public function getAssetManager() + { + return $this->_assetManager ?: Yii::$app->getAssetManager(); + } + + /** + * Sets the asset manager. + * @param \yii\web\AssetManager $value the asset manager + */ + public function setAssetManager($value) + { + $this->_assetManager = $value; + } + + /** + * Marks the beginning of an HTML page. + */ + public function beginPage() + { + ob_start(); + ob_implicit_flush(false); + } + + /** + * Marks the ending of an HTML page. + */ + public function endPage() + { + $content = ob_get_clean(); + echo strtr($content, array( + self::PL_HEAD => $this->renderHeadHtml(), + self::PL_BODY_BEGIN => $this->renderBodyBeginHtml(), + self::PL_BODY_END => $this->renderBodyEndHtml(), + )); + + unset( + $this->assetBundles, + $this->metaTags, + $this->linkTags, + $this->css, + $this->cssFiles, + $this->js, + $this->jsFiles + ); + } + + /** + * Marks the beginning of an HTML body section. + */ + public function beginBody() + { + echo self::PL_BODY_BEGIN; + } + + /** + * Marks the ending of an HTML body section. + */ + public function endBody() + { + echo self::PL_BODY_END; + } + + /** + * Marks the position of an HTML head section. + */ + public function head() + { + echo self::PL_HEAD; + } + + /** + * Registers the named asset bundle. + * All dependent asset bundles will be registered. + * @param string $name the name of the asset bundle. + * @throws InvalidConfigException if the asset bundle does not exist or a circular dependency is detected + */ + public function registerAssetBundle($name) + { + if (!isset($this->assetBundles[$name])) { + $am = $this->getAssetManager(); + $bundle = $am->getBundle($name); + if ($bundle !== null) { + $this->assetBundles[$name] = false; + $bundle->registerAssets($this); + $this->assetBundles[$name] = true; + } else { + throw new InvalidConfigException("Unknown asset bundle: $name"); + } + } elseif ($this->assetBundles[$name] === false) { + throw new InvalidConfigException("A circular dependency is detected for bundle '$name'."); + } + } + + /** + * Registers a meta tag. + * @param array $options the HTML attributes for the meta tag. + * @param string $key the key that identifies the meta tag. If two meta tags are registered + * with the same key, the latter will overwrite the former. If this is null, the new meta tag + * will be appended to the existing ones. + */ + public function registerMetaTag($options, $key = null) + { + if ($key === null) { + $this->metaTags[] = Html::tag('meta', '', $options); + } else { + $this->metaTags[$key] = Html::tag('meta', '', $options); + } + } + + /** + * Registers a link tag. + * @param array $options the HTML attributes for the link tag. + * @param string $key the key that identifies the link tag. If two link tags are registered + * with the same key, the latter will overwrite the former. If this is null, the new link tag + * will be appended to the existing ones. + */ + public function registerLinkTag($options, $key = null) + { + if ($key === null) { + $this->linkTags[] = Html::tag('link', '', $options); + } else { + $this->linkTags[$key] = Html::tag('link', '', $options); + } + } + + /** + * Registers a CSS code block. + * @param string $css the CSS code block to be registered + * @param array $options the HTML attributes for the style tag. + * @param string $key the key that identifies the CSS code block. If null, it will use + * $css as the key. If two CSS code blocks are registered with the same key, the latter + * will overwrite the former. + */ + public function registerCss($css, $options = array(), $key = null) + { + $key = $key ?: $css; + $this->css[$key] = Html::style($css, $options); + } + + /** + * Registers a CSS file. + * @param string $url the CSS file to be registered. + * @param array $options the HTML attributes for the link tag. + * @param string $key the key that identifies the CSS script file. If null, it will use + * $url as the key. If two CSS files are registered with the same key, the latter + * will overwrite the former. + */ + public function registerCssFile($url, $options = array(), $key = null) + { + $key = $key ?: $url; + $this->cssFiles[$key] = Html::cssFile($url, $options); + } + + /** + * Registers a JS code block. + * @param string $js the JS code block to be registered + * @param array $options the HTML attributes for the script tag. A special option + * named "position" is supported which specifies where the JS script tag should be inserted + * in a page. The possible values of "position" are: + * + * - [[POS_HEAD]]: in the head section + * - [[POS_BEGIN]]: at the beginning of the body section + * - [[POS_END]]: at the end of the body section + * + * @param string $key the key that identifies the JS code block. If null, it will use + * $js as the key. If two JS code blocks are registered with the same key, the latter + * will overwrite the former. + */ + public function registerJs($js, $options = array(), $key = null) + { + $position = isset($options['position']) ? $options['position'] : self::POS_END; + unset($options['position']); + $key = $key ?: $js; + $this->js[$position][$key] = Html::script($js, $options); + } + + /** + * Registers a JS file. + * @param string $url the JS file to be registered. + * @param array $options the HTML attributes for the script tag. A special option + * named "position" is supported which specifies where the JS script tag should be inserted + * in a page. The possible values of "position" are: + * + * - [[POS_HEAD]]: in the head section + * - [[POS_BEGIN]]: at the beginning of the body section + * - [[POS_END]]: at the end of the body section + * + * @param string $key the key that identifies the JS script file. If null, it will use + * $url as the key. If two JS files are registered with the same key, the latter + * will overwrite the former. + */ + public function registerJsFile($url, $options = array(), $key = null) + { + $position = isset($options['position']) ? $options['position'] : self::POS_END; + unset($options['position']); + $key = $key ?: $url; + $this->jsFiles[$position][$key] = Html::jsFile($url, $options); + } + + /** + * Renders the content to be inserted in the head section. + * The content is rendered using the registered meta tags, link tags, CSS/JS code blocks and files. + * @return string the rendered content + */ + protected function renderHeadHtml() + { + $lines = array(); + if (!empty($this->metaTags)) { + $lines[] = implode("\n", $this->cssFiles); + } + if (!empty($this->linkTags)) { + $lines[] = implode("\n", $this->cssFiles); + } + if (!empty($this->cssFiles)) { + $lines[] = implode("\n", $this->cssFiles); + } + if (!empty($this->css)) { + $lines[] = implode("\n", $this->css); + } + if (!empty($this->jsFiles[self::POS_HEAD])) { + $lines[] = implode("\n", $this->jsFiles[self::POS_HEAD]); + } + if (!empty($this->js[self::POS_HEAD])) { + $lines[] = implode("\n", $this->js[self::POS_HEAD]); + } + return implode("\n", $lines); + } + + /** + * Renders the content to be inserted at the beginning of the body section. + * The content is rendered using the registered JS code blocks and files. + * @return string the rendered content + */ + protected function renderBodyBeginHtml() + { + $lines = array(); + if (!empty($this->jsFiles[self::POS_BEGIN])) { + $lines[] = implode("\n", $this->jsFiles[self::POS_BEGIN]); + } + if (!empty($this->js[self::POS_BEGIN])) { + $lines[] = implode("\n", $this->js[self::POS_BEGIN]); + } + return implode("\n", $lines); + } + + /** + * Renders the content to be inserted at the end of the body section. + * The content is rendered using the registered JS code blocks and files. + * @return string the rendered content + */ + protected function renderBodyEndHtml() + { + $lines = array(); + if (!empty($this->jsFiles[self::POS_END])) { + $lines[] = implode("\n", $this->jsFiles[self::POS_END]); + } + if (!empty($this->js[self::POS_END])) { + $lines[] = implode("\n", $this->js[self::POS_END]); + } + return implode("\n", $lines); + } } \ No newline at end of file diff --git a/framework/base/ViewContent.php b/framework/base/ViewContent.php deleted file mode 100644 index 6a3b489..0000000 --- a/framework/base/ViewContent.php +++ /dev/null @@ -1,136 +0,0 @@ - - * @since 2.0 - */ -class ViewContent extends Component -{ - const POS_HEAD = 1; - const POS_BEGIN = 2; - const POS_END = 3; - - /** - * @var array - * - * Each asset bundle should be declared with the following structure: - * - * ~~~ - * array( - * 'basePath' => '...', - * 'baseUrl' => '...', // if missing, the bundle will be published to the "www/assets" folder - * 'js' => array( - * 'js/main.js', - * 'js/menu.js', - * 'js/base.js' => self::POS_HEAD, - * 'css' => array( - * 'css/main.css', - * 'css/menu.css', - * ), - * 'depends' => array( - * 'jquery', - * 'yii', - * 'yii/treeview', - * ), - * ) - * ~~~ - */ - public $bundles; - - public $title; - public $metaTags; - public $linkTags; - public $css; - public $js; - public $cssFiles; - public $jsFiles; - - public function populate($content) - { - return $content; - } - - public function reset() - { - $this->title = null; - $this->metaTags = null; - $this->linkTags = null; - $this->css = null; - $this->js = null; - $this->cssFiles = null; - $this->jsFiles = null; - } - - public function registerBundle($name) - { - - } - - public function getMetaTag($key) - { - return isset($this->metaTags[$key]) ? $this->metaTags[$key] : null; - } - - public function setMetaTag($key, $tag) - { - $this->metaTags[$key] = $tag; - } - - public function getLinkTag($key) - { - return isset($this->linkTags[$key]) ? $this->linkTags[$key] : null; - } - - public function setLinkTag($key, $tag) - { - $this->linkTags[$key] = $tag; - } - - public function getCss($key) - { - return isset($this->css[$key]) ? $this->css[$key]: null; - } - - public function setCss($key, $css) - { - $this->css[$key] = $css; - } - - public function getCssFile($key) - { - return isset($this->cssFiles[$key]) ? $this->cssFiles[$key]: null; - } - - public function setCssFile($key, $file) - { - $this->cssFiles[$key] = $file; - } - - public function getJs($key, $position = self::POS_END) - { - return isset($this->js[$position][$key]) ? $this->js[$position][$key] : null; - } - - public function setJs($key, $js, $position = self::POS_END) - { - $this->js[$position][$key] = $js; - } - - public function getJsFile($key, $position = self::POS_END) - { - return isset($this->jsFiles[$position][$key]) ? $this->jsFiles[$position][$key] : null; - } - - public function setJsFile($key, $file, $position = self::POS_END) - { - $this->jsFiles[$position][$key] = $file; - } - -} \ No newline at end of file diff --git a/framework/base/Widget.php b/framework/base/Widget.php index c6667fa..13e6d30 100644 --- a/framework/base/Widget.php +++ b/framework/base/Widget.php @@ -131,6 +131,6 @@ class Widget extends Component $file = $this->getViewPath() . DIRECTORY_SEPARATOR . ltrim($view, '/'); } - return FileHelper::getExtension($file) === '' ? $file . '.php' : $file; + return pathinfo($file, PATHINFO_EXTENSION) === '' ? $file . '.php' : $file; } } \ No newline at end of file diff --git a/framework/caching/ApcCache.php b/framework/caching/ApcCache.php index dd954cc..391851d 100644 --- a/framework/caching/ApcCache.php +++ b/framework/caching/ApcCache.php @@ -11,6 +11,7 @@ namespace yii\caching; * ApcCache provides APC caching in terms of an application component. * * To use this application component, the [APC PHP extension](http://www.php.net/apc) must be loaded. + * In order to enable APC for CLI you should add "apc.enable_cli = 1" to your php.ini. * * See [[Cache]] for common cache operations that ApcCache supports. * diff --git a/framework/caching/DbCache.php b/framework/caching/DbCache.php index dee8c7a..befb523 100644 --- a/framework/caching/DbCache.php +++ b/framework/caching/DbCache.php @@ -124,7 +124,7 @@ class DbCache extends Cache $query = new Query; $query->select(array('id', 'data')) ->from($this->cacheTable) - ->where(array('id' => $keys)) + ->where(array('in', 'id', (array)$keys)) ->andWhere('([[expire]] = 0 OR [[expire]] > ' . time() . ')'); if ($this->db->enableQueryCache) { diff --git a/framework/caching/DbDependency.php b/framework/caching/DbDependency.php index cbe0ae1..7d45223 100644 --- a/framework/caching/DbDependency.php +++ b/framework/caching/DbDependency.php @@ -52,6 +52,7 @@ class DbDependency extends Dependency /** * Generates the data needed to determine if dependency has been changed. * This method returns the value of the global state. + * @throws InvalidConfigException * @return mixed the data needed to determine if dependency has been changed. */ protected function generateDependencyData() diff --git a/framework/caching/FileCache.php b/framework/caching/FileCache.php index e565cad..cc1a871 100644 --- a/framework/caching/FileCache.php +++ b/framework/caching/FileCache.php @@ -7,7 +7,7 @@ namespace yii\caching; -use yii\base\InvalidConfigException; +use Yii; /** * FileCache implements a cache component using files. @@ -51,7 +51,7 @@ class FileCache extends Cache public function init() { parent::init(); - $this->cachePath = \Yii::getAlias($this->cachePath); + $this->cachePath = Yii::getAlias($this->cachePath); if (!is_dir($this->cachePath)) { mkdir($this->cachePath, 0777, true); } diff --git a/framework/caching/MemCache.php b/framework/caching/MemCache.php index df07b8e..20aff21 100644 --- a/framework/caching/MemCache.php +++ b/framework/caching/MemCache.php @@ -106,7 +106,7 @@ class MemCache extends Cache /** * Returns the underlying memcache (or memcached) object. * @return \Memcache|\Memcached the memcache (or memcached) object used by this cache component. - * @throws Exception if memcache or memcached extension is not loaded + * @throws InvalidConfigException if memcache or memcached extension is not loaded */ public function getMemcache() { diff --git a/framework/caching/ZendDataCache.php b/framework/caching/ZendDataCache.php index 669733d..5b41a8d 100644 --- a/framework/caching/ZendDataCache.php +++ b/framework/caching/ZendDataCache.php @@ -10,7 +10,7 @@ namespace yii\caching; /** * ZendDataCache provides Zend data caching in terms of an application component. * - * To use this application component, the [Zend Data Cache PHP extensionn](http://www.zend.com/en/products/server/) + * To use this application component, the [Zend Data Cache PHP extension](http://www.zend.com/en/products/server/) * must be loaded. * * See [[Cache]] for common cache operations that ZendDataCache supports. diff --git a/framework/console/Application.php b/framework/console/Application.php index 574495b..2f28cac 100644 --- a/framework/console/Application.php +++ b/framework/console/Application.php @@ -129,6 +129,7 @@ class Application extends \yii\base\Application 'migrate' => 'yii\console\controllers\MigrateController', 'app' => 'yii\console\controllers\AppController', 'cache' => 'yii\console\controllers\CacheController', + 'asset' => 'yii\console\controllers\AssetController', ); } diff --git a/framework/console/controllers/AppController.php b/framework/console/controllers/AppController.php index 93ef5f5..a47acfe 100644 --- a/framework/console/controllers/AppController.php +++ b/framework/console/controllers/AppController.php @@ -86,7 +86,7 @@ class AppController extends Controller $sourceDir = $this->getSourceDir(); $config = $this->getConfig(); - $list = FileHelper::buildFileList($sourceDir, $path); + $list = $this->buildFileList($sourceDir, $path); if(is_array($config)) { foreach($config as $file => $settings) { @@ -96,7 +96,7 @@ class AppController extends Controller } } - FileHelper::copyFiles($list); + $this->copyFiles($list); if(is_array($config)) { foreach($config as $file => $settings) { @@ -159,7 +159,7 @@ class AppController extends Controller * @param string $pathTo path to file we want to get relative path for * @param string $varName variable name w/o $ to replace value with relative path for * - * @return string target file contetns + * @return string target file contents */ public function replaceRelativePath($source, $pathTo, $varName) { @@ -204,4 +204,121 @@ class AppController extends Controller return '__DIR__.\''.$up.'/'.basename($path1).'\''; } + + + /** + * Copies a list of files from one place to another. + * @param array $fileList the list of files to be copied (name=>spec). + * The array keys are names displayed during the copy process, and array values are specifications + * for files to be copied. Each array value must be an array of the following structure: + * + * @see buildFileList + */ + protected function copyFiles($fileList) + { + $overwriteAll = false; + foreach($fileList as $name=>$file) { + $source = strtr($file['source'], '/\\', DIRECTORY_SEPARATOR); + $target = strtr($file['target'], '/\\', DIRECTORY_SEPARATOR); + $callback = isset($file['callback']) ? $file['callback'] : null; + $params = isset($file['params']) ? $file['params'] : null; + + if(is_dir($source)) { + if (!is_dir($target)) { + mkdir($target, 0777, true); + } + continue; + } + + if($callback !== null) { + $content = call_user_func($callback, $source, $params); + } + else { + $content = file_get_contents($source); + } + if(is_file($target)) { + if($content === file_get_contents($target)) { + echo " unchanged $name\n"; + continue; + } + if($overwriteAll) { + echo " overwrite $name\n"; + } + else { + echo " exist $name\n"; + echo " ...overwrite? [Yes|No|All|Quit] "; + $answer = trim(fgets(STDIN)); + if(!strncasecmp($answer, 'q', 1)) { + return; + } + elseif(!strncasecmp($answer, 'y', 1)) { + echo " overwrite $name\n"; + } + elseif(!strncasecmp($answer, 'a', 1)) { + echo " overwrite $name\n"; + $overwriteAll = true; + } + else { + echo " skip $name\n"; + continue; + } + } + } + else { + if (!is_dir(dirname($target))) { + mkdir(dirname($target), 0777, true); + } + echo " generate $name\n"; + } + file_put_contents($target, $content); + } + } + + /** + * Builds the file list of a directory. + * This method traverses through the specified directory and builds + * a list of files and subdirectories that the directory contains. + * The result of this function can be passed to {@link copyFiles}. + * @param string $sourceDir the source directory + * @param string $targetDir the target directory + * @param string $baseDir base directory + * @param array $ignoreFiles list of the names of files that should + * be ignored in list building process. + * @param array $renameMap hash array of file names that should be + * renamed. Example value: array('1.old.txt'=>'2.new.txt'). + * @return array the file list (see {@link copyFiles}) + */ + protected function buildFileList($sourceDir, $targetDir, $baseDir='', $ignoreFiles=array(), $renameMap=array()) + { + $list = array(); + $handle = opendir($sourceDir); + while(($file = readdir($handle)) !== false) { + if(in_array($file, array('.', '..', '.svn', '.gitignore', '.hgignore')) || in_array($file, $ignoreFiles)) { + continue; + } + $sourcePath = $sourceDir.DIRECTORY_SEPARATOR.$file; + $targetPath = $targetDir.DIRECTORY_SEPARATOR.strtr($file, $renameMap); + $name = $baseDir === '' ? $file : $baseDir.'/'.$file; + $list[$name] = array( + 'source' => $sourcePath, + 'target' => $targetPath, + ); + if(is_dir($sourcePath)) { + $list = array_merge($list, self::buildFileList($sourcePath, $targetPath, $name, $ignoreFiles, $renameMap)); + } + } + closedir($handle); + return $list; + } } \ No newline at end of file diff --git a/framework/console/controllers/AssetController.php b/framework/console/controllers/AssetController.php new file mode 100644 index 0000000..71a2cae --- /dev/null +++ b/framework/console/controllers/AssetController.php @@ -0,0 +1,353 @@ + + * @since 2.0 + */ +class AssetController extends Controller +{ + public $defaultAction = 'compress'; + + public $bundles = array(); + public $extensions = array(); + /** + * @var array + * ~~~ + * 'all' => array( + * 'css' => 'all.css', + * 'js' => 'js.css', + * 'depends' => array( ... ), + * ) + * ~~~ + */ + public $targets = array(); + public $assetManager = array(); + public $jsCompressor = 'java -jar compiler.jar --js {from} --js_output_file {to}'; + public $cssCompressor = 'java -jar yuicompressor.jar {from} -o {to}'; + + public function actionCompress($configFile, $bundleFile) + { + $this->loadConfiguration($configFile); + $bundles = $this->loadBundles($this->bundles, $this->extensions); + $targets = $this->loadTargets($this->targets, $bundles); + $this->publishBundles($bundles, $this->publishOptions); + $timestamp = time(); + foreach ($targets as $target) { + if (!empty($target->js)) { + $this->buildTarget($target, 'js', $bundles, $timestamp); + } + if (!empty($target->css)) { + $this->buildTarget($target, 'css', $bundles, $timestamp); + } + } + + $targets = $this->adjustDependency($targets, $bundles); + $this->saveTargets($targets, $bundleFile); + } + + protected function loadConfiguration($configFile) + { + foreach (require($configFile) as $name => $value) { + if (property_exists($this, $name)) { + $this->$name = $value; + } else { + throw new Exception("Unknown configuration option: $name"); + } + } + + if (!isset($this->assetManager['basePath'])) { + throw new Exception("Please specify 'basePath' for the 'assetManager' option."); + } + if (!isset($this->assetManager['baseUrl'])) { + throw new Exception("Please specify 'baseUrl' for the 'assetManager' option."); + } + } + + protected function loadBundles($bundles, $extensions) + { + $result = array(); + foreach ($bundles as $name => $bundle) { + $bundle['class'] = 'yii\\web\\AssetBundle'; + $result[$name] = Yii::createObject($bundle); + } + foreach ($extensions as $path) { + $manifest = $path . '/assets.php'; + if (!is_file($manifest)) { + continue; + } + foreach (require($manifest) as $name => $bundle) { + if (!isset($result[$name])) { + $bundle['class'] = 'yii\\web\\AssetBundle'; + $result[$name] = Yii::createObject($bundle); + } + } + } + return $result; + } + + protected function loadTargets($targets, $bundles) + { + // build the dependency order of bundles + $registered = array(); + foreach ($bundles as $name => $bundle) { + $this->registerBundle($bundles, $name, $registered); + } + $bundleOrders = array_combine(array_keys($registered), range(0, count($bundles) - 1)); + + // fill up the target which has empty 'depends'. + $referenced = array(); + foreach ($targets as $name => $target) { + if (empty($target['depends'])) { + if (!isset($all)) { + $all = $name; + } else { + throw new Exception("Only one target can have empty 'depends' option. Found two now: $all, $name"); + } + } else { + foreach ($target['depends'] as $bundle) { + if (!isset($referenced[$bundle])) { + $referenced[$bundle] = $name; + } else { + throw new Exception("Target '{$referenced[$bundle]}' and '$name' cannot contain the bundle '$bundle' at the same time."); + } + } + } + } + if (isset($all)) { + $targets[$all]['depends'] = array_diff(array_keys($registered), array_keys($referenced)); + } + + // adjust the 'depends' order for each target according to the dependency order of bundles + // create an AssetBundle object for each target + foreach ($targets as $name => $target) { + if (!isset($target['basePath'])) { + throw new Exception("Please specify 'basePath' for the '$name' target."); + } + if (!isset($target['baseUrl'])) { + throw new Exception("Please specify 'baseUrl' for the '$name' target."); + } + usort($target['depends'], function ($a, $b) use ($bundleOrders) { + if ($bundleOrders[$a] == $bundleOrders[$b]) { + return 0; + } else { + return $bundleOrders[$a] > $bundleOrders[$b] ? 1 : -1; + } + }); + $target['class'] = 'yii\\web\\AssetBundle'; + $targets[$name] = Yii::createObject($target); + } + return $targets; + } + + /** + * @param \yii\web\AssetBundle[] $bundles + * @param array $options + */ + protected function publishBundles($bundles, $options) + { + if (!isset($options['class'])) { + $options['class'] = 'yii\\web\\AssetManager'; + } + $am = Yii::createObject($options); + foreach ($bundles as $bundle) { + $bundle->publish($am); + } + } + + /** + * @param \yii\web\AssetBundle $target + * @param string $type either "js" or "css" + * @param \yii\web\AssetBundle[] $bundles + * @param integer $timestamp + * @throws Exception + */ + protected function buildTarget($target, $type, $bundles, $timestamp) + { + $outputFile = strtr($target->$type, array( + '{ts}' => $timestamp, + )); + $inputFiles = array(); + + foreach ($target->depends as $name) { + if (isset($bundles[$name])) { + foreach ($bundles[$name]->$type as $file) { + $inputFiles[] = $bundles[$name]->basePath . '/' . $file; + } + } else { + throw new Exception("Unknown bundle: $name"); + } + } + if ($type === 'js') { + $this->compressJsFiles($inputFiles, $target->basePath . '/' . $outputFile); + } else { + $this->compressCssFiles($inputFiles, $target->basePath . '/' . $outputFile); + } + $target->$type = array($outputFile); + } + + protected function adjustDependency($targets, $bundles) + { + $map = array(); + foreach ($targets as $name => $target) { + foreach ($target->depends as $bundle) { + $map[$bundle] = $name; + } + } + + foreach ($targets as $name => $target) { + $depends = array(); + foreach ($target->depends as $bn) { + foreach ($bundles[$bn]->depends as $bundle) { + $depends[$map[$bundle]] = true; + } + } + unset($depends[$name]); + $target->depends = array_keys($depends); + } + + // detect possible circular dependencies + foreach ($targets as $name => $target) { + $registered = array(); + $this->registerBundle($targets, $name, $registered); + } + + foreach ($map as $bundle => $target) { + $targets[$bundle] = Yii::createObject(array( + 'class' => 'yii\\web\\AssetBundle', + 'depends' => array($target), + )); + } + return $targets; + } + + protected function registerBundle($bundles, $name, &$registered) + { + if (!isset($registered[$name])) { + $registered[$name] = false; + $bundle = $bundles[$name]; + foreach ($bundle->depends as $depend) { + $this->registerBundle($bundles, $depend, $registered); + } + unset($registered[$name]); + $registered[$name] = true; + } elseif ($registered[$name] === false) { + throw new Exception("A circular dependency is detected for target '$name'."); + } + } + + protected function saveTargets($targets, $bundleFile) + { + $array = array(); + foreach ($targets as $name => $target) { + foreach (array('js', 'css', 'depends', 'basePath', 'baseUrl') as $prop) { + if (!empty($target->$prop)) { + $array[$name][$prop] = $target->$prop; + } + } + } + $array = var_export($array, true); + $version = date('Y-m-d H:i:s', time()); + file_put_contents($bundleFile, <<jsCompressor)) { + $tmpFile = $outputFile . '.tmp'; + $this->combineJsFiles($inputFiles, $tmpFile); + $log = shell_exec(strtr($this->jsCompressor, array( + '{from}' => $tmpFile, + '{to}' => $outputFile, + ))); + @unlink($tmpFile); + } else { + $log = call_user_func($this->jsCompressor, $this, $inputFiles, $outputFile); + } + } + + protected function compressCssFiles($inputFiles, $outputFile) + { + if (is_string($this->cssCompressor)) { + $tmpFile = $outputFile . '.tmp'; + $this->combineCssFiles($inputFiles, $tmpFile); + $log = shell_exec(strtr($this->cssCompressor, array( + '{from}' => $inputFiles, + '{to}' => $outputFile, + ))); + } else { + $log = call_user_func($this->cssCompressor, $this, $inputFiles, $outputFile); + } + } + + public function combineJsFiles($files, $tmpFile) + { + $content = ''; + foreach ($files as $file) { + $content .= "/*** BEGIN FILE: $file ***/\n" + . file_get_contents($file) + . "/*** END FILE: $file ***/\n"; + } + file_put_contents($tmpFile, $content); + } + + public function combineCssFiles($files, $tmpFile) + { + // todo: adjust url() references in CSS files + $content = ''; + foreach ($files as $file) { + $content .= "/*** BEGIN FILE: $file ***/\n" + . file_get_contents($file) + . "/*** END FILE: $file ***/\n"; + } + file_put_contents($tmpFile, $content); + } + + public function actionTemplate($configFile) + { + $template = << require('path/to/bundles.php'), + // + 'extensions' => require('path/to/namespaces.php'), + // + 'targets' => array( + 'all' => array( + 'basePath' => __DIR__, + 'baseUrl' => '/test', + 'js' => 'all-{ts}.js', + 'css' => 'all-{ts}.css', + ), + ), + + 'assetManager' => array( + 'basePath' => __DIR__, + 'baseUrl' => '/test', + ), +); +EOD; + file_put_contents($configFile, $template); + } +} \ No newline at end of file diff --git a/framework/console/webapp/default/index.php b/framework/console/webapp/default/index.php index 461b364..b84e257 100644 --- a/framework/console/webapp/default/index.php +++ b/framework/console/webapp/default/index.php @@ -1,10 +1,10 @@ run(); \ No newline at end of file diff --git a/framework/console/webapp/default/protected/config/main.php b/framework/console/webapp/default/protected/config/main.php index 1e3f981..795811e 100644 --- a/framework/console/webapp/default/protected/config/main.php +++ b/framework/console/webapp/default/protected/config/main.php @@ -1,5 +1,6 @@ 'webapp', 'name' => 'My Web Application', 'components' => array( @@ -12,5 +13,8 @@ return array( 'password' => '', ), */ + 'cache' => array( + 'class' => 'yii\caching\DummyCache', + ), ), ); \ No newline at end of file diff --git a/framework/console/webapp/default/protected/views/layouts/main.php b/framework/console/webapp/default/protected/views/layouts/main.php index 197b4a2..5c883e6 100644 --- a/framework/console/webapp/default/protected/views/layouts/main.php +++ b/framework/console/webapp/default/protected/views/layouts/main.php @@ -1,11 +1,12 @@ + - + - <?php echo $this->context->pageTitle?> + <?php echo Html::encode($this->title)?> -

context->pageTitle?>

+

title)?>

diff --git a/framework/db/Connection.php b/framework/db/Connection.php index 695034a..797508a 100644 --- a/framework/db/Connection.php +++ b/framework/db/Connection.php @@ -522,7 +522,7 @@ class Connection extends Component if (isset($matches[3])) { return $db->quoteColumnName($matches[3]); } else { - return str_replace('%', $this->tablePrefix, $db->quoteTableName($matches[2])); + return str_replace('%', $db->tablePrefix, $db->quoteTableName($matches[2])); } }, $sql); } diff --git a/framework/helpers/ArrayHelper.php b/framework/helpers/ArrayHelper.php index 65fa962..3061717 100644 --- a/framework/helpers/ArrayHelper.php +++ b/framework/helpers/ArrayHelper.php @@ -7,9 +7,6 @@ namespace yii\helpers; -use Yii; -use yii\base\InvalidParamException; - /** * ArrayHelper provides additional array functionality you can use in your * application. @@ -17,324 +14,6 @@ use yii\base\InvalidParamException; * @author Qiang Xue * @since 2.0 */ -class ArrayHelper +class ArrayHelper extends base\ArrayHelper { - /** - * Merges two or more arrays into one recursively. - * If each array has an element with the same string key value, the latter - * will overwrite the former (different from array_merge_recursive). - * Recursive merging will be conducted if both arrays have an element of array - * type and are having the same key. - * For integer-keyed elements, the elements from the latter array will - * be appended to the former array. - * @param array $a array to be merged to - * @param array $b array to be merged from. You can specify additional - * arrays via third argument, fourth argument etc. - * @return array the merged array (the original arrays are not changed.) - */ - public static function merge($a, $b) - { - $args = func_get_args(); - $res = array_shift($args); - while ($args !== array()) { - $next = array_shift($args); - foreach ($next as $k => $v) { - if (is_integer($k)) { - isset($res[$k]) ? $res[] = $v : $res[$k] = $v; - } elseif (is_array($v) && isset($res[$k]) && is_array($res[$k])) { - $res[$k] = self::merge($res[$k], $v); - } else { - $res[$k] = $v; - } - } - } - return $res; - } - - /** - * Retrieves the value of an array element or object property with the given key or property name. - * If the key does not exist in the array, the default value will be returned instead. - * - * Below are some usage examples, - * - * ~~~ - * // working with array - * $username = \yii\helpers\ArrayHelper::getValue($_POST, 'username'); - * // working with object - * $username = \yii\helpers\ArrayHelper::getValue($user, 'username'); - * // working with anonymous function - * $fullName = \yii\helpers\ArrayHelper::getValue($user, function($user, $defaultValue) { - * return $user->firstName . ' ' . $user->lastName; - * }); - * ~~~ - * - * @param array|object $array array or object to extract value from - * @param string|\Closure $key key name of the array element, or property name of the object, - * or an anonymous function returning the value. The anonymous function signature should be: - * `function($array, $defaultValue)`. - * @param mixed $default the default value to be returned if the specified key does not exist - * @return mixed the value of the - */ - public static function getValue($array, $key, $default = null) - { - if ($key instanceof \Closure) { - return $key($array, $default); - } elseif (is_array($array)) { - return isset($array[$key]) || array_key_exists($key, $array) ? $array[$key] : $default; - } else { - return $array->$key; - } - } - - /** - * Indexes an array according to a specified key. - * The input array should be multidimensional or an array of objects. - * - * The key can be a key name of the sub-array, a property name of object, or an anonymous - * function which returns the key value given an array element. - * - * If a key value is null, the corresponding array element will be discarded and not put in the result. - * - * For example, - * - * ~~~ - * $array = array( - * array('id' => '123', 'data' => 'abc'), - * array('id' => '345', 'data' => 'def'), - * ); - * $result = ArrayHelper::index($array, 'id'); - * // the result is: - * // array( - * // '123' => array('id' => '123', 'data' => 'abc'), - * // '345' => array('id' => '345', 'data' => 'def'), - * // ) - * - * // using anonymous function - * $result = ArrayHelper::index($array, function(element) { - * return $element['id']; - * }); - * ~~~ - * - * @param array $array the array that needs to be indexed - * @param string|\Closure $key the column name or anonymous function whose result will be used to index the array - * @return array the indexed array - */ - public static function index($array, $key) - { - $result = array(); - foreach ($array as $element) { - $value = static::getValue($element, $key); - $result[$value] = $element; - } - return $result; - } - - /** - * Returns the values of a specified column in an array. - * The input array should be multidimensional or an array of objects. - * - * For example, - * - * ~~~ - * $array = array( - * array('id' => '123', 'data' => 'abc'), - * array('id' => '345', 'data' => 'def'), - * ); - * $result = ArrayHelper::getColumn($array, 'id'); - * // the result is: array( '123', '345') - * - * // using anonymous function - * $result = ArrayHelper::getColumn($array, function(element) { - * return $element['id']; - * }); - * ~~~ - * - * @param array $array - * @param string|\Closure $name - * @param boolean $keepKeys whether to maintain the array keys. If false, the resulting array - * will be re-indexed with integers. - * @return array the list of column values - */ - public static function getColumn($array, $name, $keepKeys = true) - { - $result = array(); - if ($keepKeys) { - foreach ($array as $k => $element) { - $result[$k] = static::getValue($element, $name); - } - } else { - foreach ($array as $element) { - $result[] = static::getValue($element, $name); - } - } - - return $result; - } - - /** - * Builds a map (key-value pairs) from a multidimensional array or an array of objects. - * The `$from` and `$to` parameters specify the key names or property names to set up the map. - * Optionally, one can further group the map according to a grouping field `$group`. - * - * For example, - * - * ~~~ - * $array = array( - * array('id' => '123', 'name' => 'aaa', 'class' => 'x'), - * array('id' => '124', 'name' => 'bbb', 'class' => 'x'), - * array('id' => '345', 'name' => 'ccc', 'class' => 'y'), - * ); - * - * $result = ArrayHelper::map($array, 'id', 'name'); - * // the result is: - * // array( - * // '123' => 'aaa', - * // '124' => 'bbb', - * // '345' => 'ccc', - * // ) - * - * $result = ArrayHelper::map($array, 'id', 'name', 'class'); - * // the result is: - * // array( - * // 'x' => array( - * // '123' => 'aaa', - * // '124' => 'bbb', - * // ), - * // 'y' => array( - * // '345' => 'ccc', - * // ), - * // ) - * ~~~ - * - * @param array $array - * @param string|\Closure $from - * @param string|\Closure $to - * @param string|\Closure $group - * @return array - */ - public static function map($array, $from, $to, $group = null) - { - $result = array(); - foreach ($array as $element) { - $key = static::getValue($element, $from); - $value = static::getValue($element, $to); - if ($group !== null) { - $result[static::getValue($element, $group)][$key] = $value; - } else { - $result[$key] = $value; - } - } - return $result; - } - - /** - * Sorts an array of objects or arrays (with the same structure) by one or several keys. - * @param array $array the array to be sorted. The array will be modified after calling this method. - * @param string|\Closure|array $key the key(s) to be sorted by. This refers to a key name of the sub-array - * elements, a property name of the objects, or an anonymous function returning the values for comparison - * purpose. The anonymous function signature should be: `function($item)`. - * To sort by multiple keys, provide an array of keys here. - * @param boolean|array $ascending whether to sort in ascending or descending order. When - * sorting by multiple keys with different ascending orders, use an array of ascending flags. - * @param integer|array $sortFlag the PHP sort flag. Valid values include: - * `SORT_REGULAR`, `SORT_NUMERIC`, `SORT_STRING`, and `SORT_STRING | SORT_FLAG_CASE`. The last - * 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 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) - { - $keys = is_array($key) ? $key : array($key); - if (empty($keys) || empty($array)) { - return; - } - $n = count($keys); - if (is_scalar($ascending)) { - $ascending = array_fill(0, $n, $ascending); - } elseif (count($ascending) !== $n) { - 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 InvalidParamException('The length of $ascending parameter must be the same as that of $keys.'); - } - $args = array(); - foreach ($keys as $i => $key) { - $flag = $sortFlag[$i]; - if ($flag == (SORT_STRING | SORT_FLAG_CASE)) { - $flag = SORT_STRING; - $column = array(); - foreach (static::getColumn($array, $key) as $k => $value) { - $column[$k] = strtolower($value); - } - $args[] = $column; - } else { - $args[] = static::getColumn($array, $key); - } - $args[] = $ascending[$i] ? SORT_ASC : SORT_DESC; - $args[] = $flag; - } - $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/helpers/ConsoleColor.php b/framework/helpers/ConsoleColor.php index 429aeb1..794b9c8 100644 --- a/framework/helpers/ConsoleColor.php +++ b/framework/helpers/ConsoleColor.php @@ -18,453 +18,6 @@ namespace yii\helpers; * @author Carsten Brandt * @since 2.0 */ -class ConsoleColor +class ConsoleColor extends base\ConsoleColor { - const FG_BLACK = 30; - const FG_RED = 31; - const FG_GREEN = 32; - const FG_YELLOW = 33; - const FG_BLUE = 34; - const FG_PURPLE = 35; - const FG_CYAN = 36; - const FG_GREY = 37; - - const BG_BLACK = 40; - const BG_RED = 41; - const BG_GREEN = 42; - const BG_YELLOW = 43; - const BG_BLUE = 44; - const BG_PURPLE = 45; - const BG_CYAN = 46; - const BG_GREY = 47; - - const BOLD = 1; - const ITALIC = 3; - const UNDERLINE = 4; - const BLINK = 5; - const NEGATIVE = 7; - const CONCEALED = 8; - const CROSSED_OUT = 9; - const FRAMED = 51; - const ENCIRCLED = 52; - const OVERLINED = 53; - - /** - * Moves the terminal cursor up by sending ANSI control code CUU to the terminal. - * If the cursor is already at the edge of the screen, this has no effect. - * @param integer $rows number of rows the cursor should be moved up - */ - public static function moveCursorUp($rows=1) - { - echo "\033[" . (int) $rows . 'A'; - } - - /** - * Moves the terminal cursor down by sending ANSI control code CUD to the terminal. - * If the cursor is already at the edge of the screen, this has no effect. - * @param integer $rows number of rows the cursor should be moved down - */ - public static function moveCursorDown($rows=1) - { - echo "\033[" . (int) $rows . 'B'; - } - - /** - * Moves the terminal cursor forward by sending ANSI control code CUF to the terminal. - * If the cursor is already at the edge of the screen, this has no effect. - * @param integer $steps number of steps the cursor should be moved forward - */ - public static function moveCursorForward($steps=1) - { - echo "\033[" . (int) $steps . 'C'; - } - - /** - * Moves the terminal cursor backward by sending ANSI control code CUB to the terminal. - * If the cursor is already at the edge of the screen, this has no effect. - * @param integer $steps number of steps the cursor should be moved backward - */ - public static function moveCursorBackward($steps=1) - { - echo "\033[" . (int) $steps . 'D'; - } - - /** - * Moves the terminal cursor to the beginning of the next line by sending ANSI control code CNL to the terminal. - * @param integer $lines number of lines the cursor should be moved down - */ - public static function moveCursorNextLine($lines=1) - { - echo "\033[" . (int) $lines . 'E'; - } - - /** - * Moves the terminal cursor to the beginning of the previous line by sending ANSI control code CPL to the terminal. - * @param integer $lines number of lines the cursor should be moved up - */ - public static function moveCursorPrevLine($lines=1) - { - echo "\033[" . (int) $lines . 'F'; - } - - /** - * Moves the cursor to an absolute position given as column and row by sending ANSI control code CUP or CHA to the terminal. - * @param integer $column 1-based column number, 1 is the left edge of the screen. - * @param integer|null $row 1-based row number, 1 is the top edge of the screen. if not set, will move cursor only in current line. - */ - public static function moveCursorTo($column, $row=null) - { - if ($row === null) { - echo "\033[" . (int) $column . 'G'; - } else { - echo "\033[" . (int) $row . ';' . (int) $column . 'H'; - } - } - - /** - * Scrolls whole page up by sending ANSI control code SU to the terminal. - * New lines are added at the bottom. This is not supported by ANSI.SYS used in windows. - * @param int $lines number of lines to scroll up - */ - public static function scrollUp($lines=1) - { - echo "\033[".(int)$lines."S"; - } - - /** - * Scrolls whole page down by sending ANSI control code SD to the terminal. - * New lines are added at the top. This is not supported by ANSI.SYS used in windows. - * @param int $lines number of lines to scroll down - */ - public static function scrollDown($lines=1) - { - echo "\033[".(int)$lines."T"; - } - - /** - * Saves the current cursor position by sending ANSI control code SCP to the terminal. - * Position can then be restored with {@link restoreCursorPosition}. - */ - public static function saveCursorPosition() - { - echo "\033[s"; - } - - /** - * Restores the cursor position saved with {@link saveCursorPosition} by sending ANSI control code RCP to the terminal. - */ - public static function restoreCursorPosition() - { - echo "\033[u"; - } - - /** - * Hides the cursor by sending ANSI DECTCEM code ?25l to the terminal. - * Use {@link showCursor} to bring it back. - * Do not forget to show cursor when your application exits. Cursor might stay hidden in terminal after exit. - */ - public static function hideCursor() - { - echo "\033[?25l"; - } - - /** - * Will show a cursor again when it has been hidden by {@link hideCursor} by sending ANSI DECTCEM code ?25h to the terminal. - */ - public static function showCursor() - { - echo "\033[?25h"; - } - - /** - * Clears entire screen content by sending ANSI control code ED with argument 2 to the terminal. - * Cursor position will not be changed. - * **Note:** ANSI.SYS implementation used in windows will reset cursor position to upper left corner of the screen. - */ - public static function clearScreen() - { - echo "\033[2J"; - } - - /** - * Clears text from cursor to the beginning of the screen by sending ANSI control code ED with argument 1 to the terminal. - * Cursor position will not be changed. - */ - public static function clearScreenBeforeCursor() - { - echo "\033[1J"; - } - - /** - * Clears text from cursor to the end of the screen by sending ANSI control code ED with argument 0 to the terminal. - * Cursor position will not be changed. - */ - public static function clearScreenAfterCursor() - { - echo "\033[0J"; - } - - /** - * Clears the line, the cursor is currently on by sending ANSI control code EL with argument 2 to the terminal. - * Cursor position will not be changed. - */ - public static function clearLine() - { - echo "\033[2K"; - } - - /** - * Clears text from cursor position to the beginning of the line by sending ANSI control code EL with argument 1 to the terminal. - * Cursor position will not be changed. - */ - public static function clearLineBeforeCursor() - { - echo "\033[1K"; - } - - /** - * Clears text from cursor position to the end of the line by sending ANSI control code EL with argument 0 to the terminal. - * Cursor position will not be changed. - */ - public static function clearLineAfterCursor() - { - echo "\033[0K"; - } - - /** - * Will send ANSI format for following output - * - * You can pass any of the FG_*, BG_* and TEXT_* constants and also xterm256ColorBg - * TODO: documentation - */ - public static function ansiStyle() - { - echo "\033[" . implode(';', func_get_args()) . 'm'; - } - - /** - * Will return a string formatted with the given ANSI style - * - * See {@link ansiStyle} for possible arguments. - * @param string $string the string to be formatted - * @return string - */ - public static function ansiStyleString($string) - { - $args = func_get_args(); - array_shift($args); - $code = implode(';', $args); - return "\033[0m" . ($code !== '' ? "\033[" . $code . "m" : '') . $string."\033[0m"; - } - - //const COLOR_XTERM256 = 38;// http://en.wikipedia.org/wiki/Talk:ANSI_escape_code#xterm-256colors - public static function xterm256ColorFg($i) // TODO naming! - { - return '38;5;'.$i; - } - - public static function xterm256ColorBg($i) // TODO naming! - { - return '48;5;'.$i; - } - - /** - * Usage: list($w, $h) = ConsoleHelper::getScreenSize(); - * - * @return array - */ - public static function getScreenSize() - { - // TODO implement - return array(150,50); - } - - /** - * resets any ansi style set by previous method {@link ansiStyle} - * Any output after this is will have default text style. - */ - public static function reset() - { - echo "\033[0m"; - } - - /** - * Strips ANSI control codes from a string - * - * @param string $string String to strip - * @return string - */ - public static function strip($string) - { - return preg_replace('/\033\[[\d;]+m/', '', $string); // TODO currently only strips color - } - - // TODO refactor and review - public static function ansiToHtml($string) - { - $tags = 0; - return preg_replace_callback('/\033\[[\d;]+m/', function($ansi) use (&$tags) { - $styleA = array(); - foreach(explode(';', $ansi) as $controlCode) - { - switch($controlCode) - { - case static::FG_BLACK: $style = array('color' => '#000000'); break; - case static::FG_BLUE: $style = array('color' => '#000078'); break; - case static::FG_CYAN: $style = array('color' => '#007878'); break; - case static::FG_GREEN: $style = array('color' => '#007800'); break; - case static::FG_GREY: $style = array('color' => '#787878'); break; - case static::FG_PURPLE: $style = array('color' => '#780078'); break; - case static::FG_RED: $style = array('color' => '#780000'); break; - case static::FG_YELLOW: $style = array('color' => '#787800'); break; - case static::BG_BLACK: $style = array('background-color' => '#000000'); break; - case static::BG_BLUE: $style = array('background-color' => '#000078'); break; - case static::BG_CYAN: $style = array('background-color' => '#007878'); break; - case static::BG_GREEN: $style = array('background-color' => '#007800'); break; - case static::BG_GREY: $style = array('background-color' => '#787878'); break; - case static::BG_PURPLE: $style = array('background-color' => '#780078'); break; - case static::BG_RED: $style = array('background-color' => '#780000'); break; - case static::BG_YELLOW: $style = array('background-color' => '#787800'); break; - case static::BOLD: $style = array('font-weight' => 'bold'); break; - case static::ITALIC: $style = array('font-style' => 'italic'); break; - case static::UNDERLINE: $style = array('text-decoration' => array('underline')); break; - case static::OVERLINED: $style = array('text-decoration' => array('overline')); break; - case static::CROSSED_OUT:$style = array('text-decoration' => array('line-through')); break; - case static::BLINK: $style = array('text-decoration' => array('blink')); break; - case static::NEGATIVE: // ??? - case static::CONCEALED: - case static::ENCIRCLED: - case static::FRAMED: - // TODO allow resetting codes - break; - case 0: // ansi reset - $return = ''; - for($n=$tags; $tags>0; $tags--) { - $return .= ''; - } - return $return; - } - - $styleA = ArrayHelper::merge($styleA, $style); - } - $styleString[] = array(); - foreach($styleA as $name => $content) { - if ($name === 'text-decoration') { - $content = implode(' ', $content); - } - $styleString[] = $name.':'.$content; - } - $tags++; - return ' $ds, '\\' => $ds)), $ds); - } - - /** - * Returns the localized version of a specified file. - * - * The searching is based on the specified language code. In particular, - * a file with the same name will be looked for under the subdirectory - * whose name is same as the language code. For example, given the file "path/to/view.php" - * and language code "zh_cn", the localized file will be looked for as - * "path/to/zh_cn/view.php". If the file is not found, the original file - * will be returned. - * - * If the target and the source language codes are the same, - * the original file will be returned. - * - * For consistency, it is recommended that the language code is given - * in lower case and in the format of LanguageID_RegionID (e.g. "en_us"). - * - * @param string $file the original file - * @param string $language the target language that the file should be localized to. - * If not set, the value of [[\yii\base\Application::language]] will be used. - * @param string $sourceLanguage the language that the original file is in. - * If not set, the value of [[\yii\base\Application::sourceLanguage]] will be used. - * @return string the matching localized file, or the original file if the localized version is not found. - * If the target and the source language codes are the same, the original file will be returned. - */ - public static function localize($file, $language = null, $sourceLanguage = null) - { - if ($language === null) { - $language = \Yii::$app->language; - } - if ($sourceLanguage === null) { - $sourceLanguage = \Yii::$app->sourceLanguage; - } - if ($language === $sourceLanguage) { - return $file; - } - $desiredFile = dirname($file) . DIRECTORY_SEPARATOR . $sourceLanguage . DIRECTORY_SEPARATOR . basename($file); - return is_file($desiredFile) ? $desiredFile : $file; - } - - /** - * Determines the MIME type of the specified file. - * This method will first try to determine the MIME type based on - * [finfo_open](http://php.net/manual/en/function.finfo-open.php). If this doesn't work, it will - * fall back to [[getMimeTypeByExtension()]]. - * @param string $file the file name. - * @param string $magicFile name of the optional magic database file, usually something like `/path/to/magic.mime`. - * This will be passed as the second parameter to [finfo_open](http://php.net/manual/en/function.finfo-open.php). - * @param boolean $checkExtension whether to use the file extension to determine the MIME type in case - * `finfo_open()` cannot determine it. - * @return string the MIME type (e.g. `text/plain`). Null is returned if the MIME type cannot be determined. - */ - public static function getMimeType($file, $magicFile = null, $checkExtension = true) - { - if (function_exists('finfo_open')) { - $info = finfo_open(FILEINFO_MIME_TYPE, $magicFile); - if ($info && ($result = finfo_file($info, $file)) !== false) { - return $result; - } - } - - return $checkExtension ? self::getMimeTypeByExtension($file) : null; - } - - /** - * Determines the MIME type based on the extension name of the specified file. - * This method will use a local map between extension names and MIME types. - * @param string $file the file name. - * @param string $magicFile the path of the file that contains all available MIME type information. - * If this is not set, the default file aliased by `@yii/util/mimeTypes.php` will be used. - * @return string the MIME type. Null is returned if the MIME type cannot be determined. - */ - public static function getMimeTypeByExtension($file, $magicFile = null) - { - if ($magicFile === null) { - $magicFile = \Yii::getAlias('@yii/util/mimeTypes.php'); - } - $mimeTypes = require($magicFile); - if (($ext = pathinfo($file, PATHINFO_EXTENSION)) !== '') { - $ext = strtolower($ext); - if (isset($mimeTypes[$ext])) { - return $mimeTypes[$ext]; - } - } - return null; - } - - /** - * Copies a list of files from one place to another. - * @param array $fileList the list of files to be copied (name=>spec). - * The array keys are names displayed during the copy process, and array values are specifications - * for files to be copied. Each array value must be an array of the following structure: - *
    - *
  • source: required, the full path of the file/directory to be copied from
  • - *
  • target: required, the full path of the file/directory to be copied to
  • - *
  • callback: optional, the callback to be invoked when copying a file. The callback function - * should be declared as follows: - *
    -	 *   function foo($source,$params)
    -	 *   
    - * where $source parameter is the source file path, and the content returned - * by the function will be saved into the target file.
  • - *
  • params: optional, the parameters to be passed to the callback
  • - *
- * @see buildFileList - */ - public static function copyFiles($fileList) - { - $overwriteAll = false; - foreach($fileList as $name=>$file) { - $source = strtr($file['source'], '/\\', DIRECTORY_SEPARATOR); - $target = strtr($file['target'], '/\\', DIRECTORY_SEPARATOR); - $callback = isset($file['callback']) ? $file['callback'] : null; - $params = isset($file['params']) ? $file['params'] : null; - - if(is_dir($source)) { - try { - self::ensureDirectory($target); - } - catch (Exception $e) { - mkdir($target, true, 0777); - } - continue; - } - - if($callback !== null) { - $content = call_user_func($callback, $source, $params); - } - else { - $content = file_get_contents($source); - } - if(is_file($target)) { - if($content === file_get_contents($target)) { - echo " unchanged $name\n"; - continue; - } - if($overwriteAll) { - echo " overwrite $name\n"; - } - else { - echo " exist $name\n"; - echo " ...overwrite? [Yes|No|All|Quit] "; - $answer = trim(fgets(STDIN)); - if(!strncasecmp($answer, 'q', 1)) { - return; - } - elseif(!strncasecmp($answer, 'y', 1)) { - echo " overwrite $name\n"; - } - elseif(!strncasecmp($answer, 'a', 1)) { - echo " overwrite $name\n"; - $overwriteAll = true; - } - else { - echo " skip $name\n"; - continue; - } - } - } - else { - try { - self::ensureDirectory(dirname($target)); - } - catch (Exception $e) { - mkdir(dirname($target), true, 0777); - } - echo " generate $name\n"; - } - file_put_contents($target, $content); - } - } - - /** - * Builds the file list of a directory. - * This method traverses through the specified directory and builds - * a list of files and subdirectories that the directory contains. - * The result of this function can be passed to {@link copyFiles}. - * @param string $sourceDir the source directory - * @param string $targetDir the target directory - * @param string $baseDir base directory - * @param array $ignoreFiles list of the names of files that should - * be ignored in list building process. Argument available since 1.1.11. - * @param array $renameMap hash array of file names that should be - * renamed. Example value: array('1.old.txt'=>'2.new.txt'). - * @return array the file list (see {@link copyFiles}) - */ - public static function buildFileList($sourceDir, $targetDir, $baseDir='', $ignoreFiles=array(), $renameMap=array()) - { - $list = array(); - $handle = opendir($sourceDir); - while(($file = readdir($handle)) !== false) { - if(in_array($file, array('.', '..', '.svn', '.gitignore')) || in_array($file, $ignoreFiles)) { - continue; - } - $sourcePath = $sourceDir.DIRECTORY_SEPARATOR.$file; - $targetPath = $targetDir.DIRECTORY_SEPARATOR.strtr($file, $renameMap); - $name = $baseDir === '' ? $file : $baseDir.'/'.$file; - $list[$name] = array( - 'source' => $sourcePath, - 'target' => $targetPath, - ); - if(is_dir($sourcePath)) { - $list = array_merge($list, self::buildFileList($sourcePath, $targetPath, $name, $ignoreFiles, $renameMap)); - } - } - closedir($handle); - return $list; - } } \ No newline at end of file diff --git a/framework/helpers/Html.php b/framework/helpers/Html.php index b2ca576..b3a0743 100644 --- a/framework/helpers/Html.php +++ b/framework/helpers/Html.php @@ -7,975 +7,12 @@ namespace yii\helpers; -use Yii; -use yii\base\InvalidParamException; - /** * Html provides a set of static methods for generating commonly used HTML tags. * * @author Qiang Xue * @since 2.0 */ -class Html +class Html extends base\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()]] and returned; - * - is an array: the first array element is considered a route, while the rest of the name-value - * pairs are treated as the parameters to be used for URL creation using [[\yii\web\Controller::createUrl()]]. - * For example: `array('post/index', 'page' => 2)`, `array('index')`. - * - * @param array|string $url the parameter to be used to generate a valid URL - * @return string the normalized URL - * @throws InvalidParamException if the parameter is invalid. - */ - public static function url($url) - { - if (is_array($url)) { - if (isset($url[0])) { - $route = $url[0]; - $params = array_splice($url, 1); - if (Yii::$app->controller !== null) { - return Yii::$app->controller->createUrl($route, $params); - } else { - return Yii::$app->getUrlManager()->createUrl($route, $params); - } - } else { - throw new InvalidParamException('The array specifying a URL must contain at least one element.'); - } - } elseif ($url === '') { - return Yii::$app->getRequest()->getUrl(); - } else { - return Yii::getAlias($url); - } - } } diff --git a/framework/helpers/SecurityHelper.php b/framework/helpers/SecurityHelper.php index 5029dd6..d3cb2ad 100644 --- a/framework/helpers/SecurityHelper.php +++ b/framework/helpers/SecurityHelper.php @@ -7,11 +7,6 @@ namespace yii\helpers; -use Yii; -use yii\base\Exception; -use yii\base\InvalidConfigException; -use yii\base\InvalidParamException; - /** * SecurityHelper provides a set of methods to handle common security-related tasks. * @@ -29,244 +24,6 @@ use yii\base\InvalidParamException; * @author Tom Worster * @since 2.0 */ -class SecurityHelper +class SecurityHelper extends base\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/helpers/StringHelper.php b/framework/helpers/StringHelper.php index ace34db..22b881a 100644 --- a/framework/helpers/StringHelper.php +++ b/framework/helpers/StringHelper.php @@ -14,112 +14,6 @@ namespace yii\helpers; * @author Alex Makarov * @since 2.0 */ -class StringHelper +class StringHelper extends base\StringHelper { - /** - * Returns the number of bytes in the given string. - * This method ensures the string is treated as a byte array. - * It will use `mb_strlen()` if it is available. - * @param string $string the string being measured for length - * @return integer the number of bytes in the given string. - */ - public static function strlen($string) - { - return function_exists('mb_strlen') ? mb_strlen($string, '8bit') : strlen($string); - } - - /** - * Returns the portion of string specified by the start and length parameters. - * This method ensures the string is treated as a byte array. - * It will use `mb_substr()` if it is available. - * @param string $string the input string. Must be one character or longer. - * @param integer $start the starting position - * @param integer $length the desired portion length - * @return string the extracted part of string, or FALSE on failure or an empty string. - * @see http://www.php.net/manual/en/function.substr.php - */ - public static function substr($string, $start, $length) - { - return function_exists('mb_substr') ? mb_substr($string, $start, $length, '8bit') : substr($string, $start, $length); - } - - /** - * Converts a word to its plural form. - * Note that this is for English only! - * For example, 'apple' will become 'apples', and 'child' will become 'children'. - * @param string $name the word to be pluralized - * @return string the pluralized word - */ - public static function pluralize($name) - { - static $rules = array( - '/(m)ove$/i' => '\1oves', - '/(f)oot$/i' => '\1eet', - '/(c)hild$/i' => '\1hildren', - '/(h)uman$/i' => '\1umans', - '/(m)an$/i' => '\1en', - '/(s)taff$/i' => '\1taff', - '/(t)ooth$/i' => '\1eeth', - '/(p)erson$/i' => '\1eople', - '/([m|l])ouse$/i' => '\1ice', - '/(x|ch|ss|sh|us|as|is|os)$/i' => '\1es', - '/([^aeiouy]|qu)y$/i' => '\1ies', - '/(?:([^f])fe|([lr])f)$/i' => '\1\2ves', - '/(shea|lea|loa|thie)f$/i' => '\1ves', - '/([ti])um$/i' => '\1a', - '/(tomat|potat|ech|her|vet)o$/i' => '\1oes', - '/(bu)s$/i' => '\1ses', - '/(ax|test)is$/i' => '\1es', - '/s$/' => 's', - ); - foreach ($rules as $rule => $replacement) { - if (preg_match($rule, $name)) { - return preg_replace($rule, $replacement, $name); - } - } - return $name . 's'; - } - - /** - * Converts a CamelCase name into space-separated words. - * For example, 'PostTag' will be converted to 'Post Tag'. - * @param string $name the string to be converted - * @param boolean $ucwords whether to capitalize the first letter in each word - * @return string the resulting words - */ - public static function camel2words($name, $ucwords = true) - { - $label = trim(strtolower(str_replace(array('-', '_', '.'), ' ', preg_replace('/(? * @since 2.0 */ -class CVarDumper +class VarDumper extends base\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(...)'; - } 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/helpers/base/ArrayHelper.php b/framework/helpers/base/ArrayHelper.php new file mode 100644 index 0000000..9870542 --- /dev/null +++ b/framework/helpers/base/ArrayHelper.php @@ -0,0 +1,340 @@ + + * @since 2.0 + */ +class ArrayHelper +{ + /** + * Merges two or more arrays into one recursively. + * If each array has an element with the same string key value, the latter + * will overwrite the former (different from array_merge_recursive). + * Recursive merging will be conducted if both arrays have an element of array + * type and are having the same key. + * For integer-keyed elements, the elements from the latter array will + * be appended to the former array. + * @param array $a array to be merged to + * @param array $b array to be merged from. You can specify additional + * arrays via third argument, fourth argument etc. + * @return array the merged array (the original arrays are not changed.) + */ + public static function merge($a, $b) + { + $args = func_get_args(); + $res = array_shift($args); + while ($args !== array()) { + $next = array_shift($args); + foreach ($next as $k => $v) { + if (is_integer($k)) { + isset($res[$k]) ? $res[] = $v : $res[$k] = $v; + } elseif (is_array($v) && isset($res[$k]) && is_array($res[$k])) { + $res[$k] = self::merge($res[$k], $v); + } else { + $res[$k] = $v; + } + } + } + return $res; + } + + /** + * Retrieves the value of an array element or object property with the given key or property name. + * If the key does not exist in the array, the default value will be returned instead. + * + * Below are some usage examples, + * + * ~~~ + * // working with array + * $username = \yii\helpers\ArrayHelper::getValue($_POST, 'username'); + * // working with object + * $username = \yii\helpers\ArrayHelper::getValue($user, 'username'); + * // working with anonymous function + * $fullName = \yii\helpers\ArrayHelper::getValue($user, function($user, $defaultValue) { + * return $user->firstName . ' ' . $user->lastName; + * }); + * ~~~ + * + * @param array|object $array array or object to extract value from + * @param string|\Closure $key key name of the array element, or property name of the object, + * or an anonymous function returning the value. The anonymous function signature should be: + * `function($array, $defaultValue)`. + * @param mixed $default the default value to be returned if the specified key does not exist + * @return mixed the value of the + */ + public static function getValue($array, $key, $default = null) + { + if ($key instanceof \Closure) { + return $key($array, $default); + } elseif (is_array($array)) { + return isset($array[$key]) || array_key_exists($key, $array) ? $array[$key] : $default; + } else { + return $array->$key; + } + } + + /** + * Indexes an array according to a specified key. + * The input array should be multidimensional or an array of objects. + * + * The key can be a key name of the sub-array, a property name of object, or an anonymous + * function which returns the key value given an array element. + * + * If a key value is null, the corresponding array element will be discarded and not put in the result. + * + * For example, + * + * ~~~ + * $array = array( + * array('id' => '123', 'data' => 'abc'), + * array('id' => '345', 'data' => 'def'), + * ); + * $result = ArrayHelper::index($array, 'id'); + * // the result is: + * // array( + * // '123' => array('id' => '123', 'data' => 'abc'), + * // '345' => array('id' => '345', 'data' => 'def'), + * // ) + * + * // using anonymous function + * $result = ArrayHelper::index($array, function(element) { + * return $element['id']; + * }); + * ~~~ + * + * @param array $array the array that needs to be indexed + * @param string|\Closure $key the column name or anonymous function whose result will be used to index the array + * @return array the indexed array + */ + public static function index($array, $key) + { + $result = array(); + foreach ($array as $element) { + $value = static::getValue($element, $key); + $result[$value] = $element; + } + return $result; + } + + /** + * Returns the values of a specified column in an array. + * The input array should be multidimensional or an array of objects. + * + * For example, + * + * ~~~ + * $array = array( + * array('id' => '123', 'data' => 'abc'), + * array('id' => '345', 'data' => 'def'), + * ); + * $result = ArrayHelper::getColumn($array, 'id'); + * // the result is: array( '123', '345') + * + * // using anonymous function + * $result = ArrayHelper::getColumn($array, function(element) { + * return $element['id']; + * }); + * ~~~ + * + * @param array $array + * @param string|\Closure $name + * @param boolean $keepKeys whether to maintain the array keys. If false, the resulting array + * will be re-indexed with integers. + * @return array the list of column values + */ + public static function getColumn($array, $name, $keepKeys = true) + { + $result = array(); + if ($keepKeys) { + foreach ($array as $k => $element) { + $result[$k] = static::getValue($element, $name); + } + } else { + foreach ($array as $element) { + $result[] = static::getValue($element, $name); + } + } + + return $result; + } + + /** + * Builds a map (key-value pairs) from a multidimensional array or an array of objects. + * The `$from` and `$to` parameters specify the key names or property names to set up the map. + * Optionally, one can further group the map according to a grouping field `$group`. + * + * For example, + * + * ~~~ + * $array = array( + * array('id' => '123', 'name' => 'aaa', 'class' => 'x'), + * array('id' => '124', 'name' => 'bbb', 'class' => 'x'), + * array('id' => '345', 'name' => 'ccc', 'class' => 'y'), + * ); + * + * $result = ArrayHelper::map($array, 'id', 'name'); + * // the result is: + * // array( + * // '123' => 'aaa', + * // '124' => 'bbb', + * // '345' => 'ccc', + * // ) + * + * $result = ArrayHelper::map($array, 'id', 'name', 'class'); + * // the result is: + * // array( + * // 'x' => array( + * // '123' => 'aaa', + * // '124' => 'bbb', + * // ), + * // 'y' => array( + * // '345' => 'ccc', + * // ), + * // ) + * ~~~ + * + * @param array $array + * @param string|\Closure $from + * @param string|\Closure $to + * @param string|\Closure $group + * @return array + */ + public static function map($array, $from, $to, $group = null) + { + $result = array(); + foreach ($array as $element) { + $key = static::getValue($element, $from); + $value = static::getValue($element, $to); + if ($group !== null) { + $result[static::getValue($element, $group)][$key] = $value; + } else { + $result[$key] = $value; + } + } + return $result; + } + + /** + * Sorts an array of objects or arrays (with the same structure) by one or several keys. + * @param array $array the array to be sorted. The array will be modified after calling this method. + * @param string|\Closure|array $key the key(s) to be sorted by. This refers to a key name of the sub-array + * elements, a property name of the objects, or an anonymous function returning the values for comparison + * purpose. The anonymous function signature should be: `function($item)`. + * To sort by multiple keys, provide an array of keys here. + * @param boolean|array $ascending whether to sort in ascending or descending order. When + * sorting by multiple keys with different ascending orders, use an array of ascending flags. + * @param integer|array $sortFlag the PHP sort flag. Valid values include: + * `SORT_REGULAR`, `SORT_NUMERIC`, `SORT_STRING`, and `SORT_STRING | SORT_FLAG_CASE`. The last + * 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 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) + { + $keys = is_array($key) ? $key : array($key); + if (empty($keys) || empty($array)) { + return; + } + $n = count($keys); + if (is_scalar($ascending)) { + $ascending = array_fill(0, $n, $ascending); + } elseif (count($ascending) !== $n) { + 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 InvalidParamException('The length of $ascending parameter must be the same as that of $keys.'); + } + $args = array(); + foreach ($keys as $i => $key) { + $flag = $sortFlag[$i]; + if ($flag == (SORT_STRING | SORT_FLAG_CASE)) { + $flag = SORT_STRING; + $column = array(); + foreach (static::getColumn($array, $key) as $k => $value) { + $column[$k] = strtolower($value); + } + $args[] = $column; + } else { + $args[] = static::getColumn($array, $key); + } + $args[] = $ascending[$i] ? SORT_ASC : SORT_DESC; + $args[] = $flag; + } + $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/helpers/base/ConsoleColor.php b/framework/helpers/base/ConsoleColor.php new file mode 100644 index 0000000..5e7f577 --- /dev/null +++ b/framework/helpers/base/ConsoleColor.php @@ -0,0 +1,470 @@ + + * @since 2.0 + */ +class ConsoleColor +{ + const FG_BLACK = 30; + const FG_RED = 31; + const FG_GREEN = 32; + const FG_YELLOW = 33; + const FG_BLUE = 34; + const FG_PURPLE = 35; + const FG_CYAN = 36; + const FG_GREY = 37; + + const BG_BLACK = 40; + const BG_RED = 41; + const BG_GREEN = 42; + const BG_YELLOW = 43; + const BG_BLUE = 44; + const BG_PURPLE = 45; + const BG_CYAN = 46; + const BG_GREY = 47; + + const BOLD = 1; + const ITALIC = 3; + const UNDERLINE = 4; + const BLINK = 5; + const NEGATIVE = 7; + const CONCEALED = 8; + const CROSSED_OUT = 9; + const FRAMED = 51; + const ENCIRCLED = 52; + const OVERLINED = 53; + + /** + * Moves the terminal cursor up by sending ANSI control code CUU to the terminal. + * If the cursor is already at the edge of the screen, this has no effect. + * @param integer $rows number of rows the cursor should be moved up + */ + public static function moveCursorUp($rows=1) + { + echo "\033[" . (int) $rows . 'A'; + } + + /** + * Moves the terminal cursor down by sending ANSI control code CUD to the terminal. + * If the cursor is already at the edge of the screen, this has no effect. + * @param integer $rows number of rows the cursor should be moved down + */ + public static function moveCursorDown($rows=1) + { + echo "\033[" . (int) $rows . 'B'; + } + + /** + * Moves the terminal cursor forward by sending ANSI control code CUF to the terminal. + * If the cursor is already at the edge of the screen, this has no effect. + * @param integer $steps number of steps the cursor should be moved forward + */ + public static function moveCursorForward($steps=1) + { + echo "\033[" . (int) $steps . 'C'; + } + + /** + * Moves the terminal cursor backward by sending ANSI control code CUB to the terminal. + * If the cursor is already at the edge of the screen, this has no effect. + * @param integer $steps number of steps the cursor should be moved backward + */ + public static function moveCursorBackward($steps=1) + { + echo "\033[" . (int) $steps . 'D'; + } + + /** + * Moves the terminal cursor to the beginning of the next line by sending ANSI control code CNL to the terminal. + * @param integer $lines number of lines the cursor should be moved down + */ + public static function moveCursorNextLine($lines=1) + { + echo "\033[" . (int) $lines . 'E'; + } + + /** + * Moves the terminal cursor to the beginning of the previous line by sending ANSI control code CPL to the terminal. + * @param integer $lines number of lines the cursor should be moved up + */ + public static function moveCursorPrevLine($lines=1) + { + echo "\033[" . (int) $lines . 'F'; + } + + /** + * Moves the cursor to an absolute position given as column and row by sending ANSI control code CUP or CHA to the terminal. + * @param integer $column 1-based column number, 1 is the left edge of the screen. + * @param integer|null $row 1-based row number, 1 is the top edge of the screen. if not set, will move cursor only in current line. + */ + public static function moveCursorTo($column, $row=null) + { + if ($row === null) { + echo "\033[" . (int) $column . 'G'; + } else { + echo "\033[" . (int) $row . ';' . (int) $column . 'H'; + } + } + + /** + * Scrolls whole page up by sending ANSI control code SU to the terminal. + * New lines are added at the bottom. This is not supported by ANSI.SYS used in windows. + * @param int $lines number of lines to scroll up + */ + public static function scrollUp($lines=1) + { + echo "\033[".(int)$lines."S"; + } + + /** + * Scrolls whole page down by sending ANSI control code SD to the terminal. + * New lines are added at the top. This is not supported by ANSI.SYS used in windows. + * @param int $lines number of lines to scroll down + */ + public static function scrollDown($lines=1) + { + echo "\033[".(int)$lines."T"; + } + + /** + * Saves the current cursor position by sending ANSI control code SCP to the terminal. + * Position can then be restored with {@link restoreCursorPosition}. + */ + public static function saveCursorPosition() + { + echo "\033[s"; + } + + /** + * Restores the cursor position saved with {@link saveCursorPosition} by sending ANSI control code RCP to the terminal. + */ + public static function restoreCursorPosition() + { + echo "\033[u"; + } + + /** + * Hides the cursor by sending ANSI DECTCEM code ?25l to the terminal. + * Use {@link showCursor} to bring it back. + * Do not forget to show cursor when your application exits. Cursor might stay hidden in terminal after exit. + */ + public static function hideCursor() + { + echo "\033[?25l"; + } + + /** + * Will show a cursor again when it has been hidden by {@link hideCursor} by sending ANSI DECTCEM code ?25h to the terminal. + */ + public static function showCursor() + { + echo "\033[?25h"; + } + + /** + * Clears entire screen content by sending ANSI control code ED with argument 2 to the terminal. + * Cursor position will not be changed. + * **Note:** ANSI.SYS implementation used in windows will reset cursor position to upper left corner of the screen. + */ + public static function clearScreen() + { + echo "\033[2J"; + } + + /** + * Clears text from cursor to the beginning of the screen by sending ANSI control code ED with argument 1 to the terminal. + * Cursor position will not be changed. + */ + public static function clearScreenBeforeCursor() + { + echo "\033[1J"; + } + + /** + * Clears text from cursor to the end of the screen by sending ANSI control code ED with argument 0 to the terminal. + * Cursor position will not be changed. + */ + public static function clearScreenAfterCursor() + { + echo "\033[0J"; + } + + /** + * Clears the line, the cursor is currently on by sending ANSI control code EL with argument 2 to the terminal. + * Cursor position will not be changed. + */ + public static function clearLine() + { + echo "\033[2K"; + } + + /** + * Clears text from cursor position to the beginning of the line by sending ANSI control code EL with argument 1 to the terminal. + * Cursor position will not be changed. + */ + public static function clearLineBeforeCursor() + { + echo "\033[1K"; + } + + /** + * Clears text from cursor position to the end of the line by sending ANSI control code EL with argument 0 to the terminal. + * Cursor position will not be changed. + */ + public static function clearLineAfterCursor() + { + echo "\033[0K"; + } + + /** + * Will send ANSI format for following output + * + * You can pass any of the FG_*, BG_* and TEXT_* constants and also xterm256ColorBg + * TODO: documentation + */ + public static function ansiStyle() + { + echo "\033[" . implode(';', func_get_args()) . 'm'; + } + + /** + * Will return a string formatted with the given ANSI style + * + * See {@link ansiStyle} for possible arguments. + * @param string $string the string to be formatted + * @return string + */ + public static function ansiStyleString($string) + { + $args = func_get_args(); + array_shift($args); + $code = implode(';', $args); + return "\033[0m" . ($code !== '' ? "\033[" . $code . "m" : '') . $string."\033[0m"; + } + + //const COLOR_XTERM256 = 38;// http://en.wikipedia.org/wiki/Talk:ANSI_escape_code#xterm-256colors + public static function xterm256ColorFg($i) // TODO naming! + { + return '38;5;'.$i; + } + + public static function xterm256ColorBg($i) // TODO naming! + { + return '48;5;'.$i; + } + + /** + * Usage: list($w, $h) = ConsoleHelper::getScreenSize(); + * + * @return array + */ + public static function getScreenSize() + { + // TODO implement + return array(150,50); + } + + /** + * resets any ansi style set by previous method {@link ansiStyle} + * Any output after this is will have default text style. + */ + public static function reset() + { + echo "\033[0m"; + } + + /** + * Strips ANSI control codes from a string + * + * @param string $string String to strip + * @return string + */ + public static function strip($string) + { + return preg_replace('/\033\[[\d;]+m/', '', $string); // TODO currently only strips color + } + + // TODO refactor and review + public static function ansiToHtml($string) + { + $tags = 0; + return preg_replace_callback('/\033\[[\d;]+m/', function($ansi) use (&$tags) { + $styleA = array(); + foreach(explode(';', $ansi) as $controlCode) + { + switch($controlCode) + { + case static::FG_BLACK: $style = array('color' => '#000000'); break; + case static::FG_BLUE: $style = array('color' => '#000078'); break; + case static::FG_CYAN: $style = array('color' => '#007878'); break; + case static::FG_GREEN: $style = array('color' => '#007800'); break; + case static::FG_GREY: $style = array('color' => '#787878'); break; + case static::FG_PURPLE: $style = array('color' => '#780078'); break; + case static::FG_RED: $style = array('color' => '#780000'); break; + case static::FG_YELLOW: $style = array('color' => '#787800'); break; + case static::BG_BLACK: $style = array('background-color' => '#000000'); break; + case static::BG_BLUE: $style = array('background-color' => '#000078'); break; + case static::BG_CYAN: $style = array('background-color' => '#007878'); break; + case static::BG_GREEN: $style = array('background-color' => '#007800'); break; + case static::BG_GREY: $style = array('background-color' => '#787878'); break; + case static::BG_PURPLE: $style = array('background-color' => '#780078'); break; + case static::BG_RED: $style = array('background-color' => '#780000'); break; + case static::BG_YELLOW: $style = array('background-color' => '#787800'); break; + case static::BOLD: $style = array('font-weight' => 'bold'); break; + case static::ITALIC: $style = array('font-style' => 'italic'); break; + case static::UNDERLINE: $style = array('text-decoration' => array('underline')); break; + case static::OVERLINED: $style = array('text-decoration' => array('overline')); break; + case static::CROSSED_OUT:$style = array('text-decoration' => array('line-through')); break; + case static::BLINK: $style = array('text-decoration' => array('blink')); break; + case static::NEGATIVE: // ??? + case static::CONCEALED: + case static::ENCIRCLED: + case static::FRAMED: + // TODO allow resetting codes + break; + case 0: // ansi reset + $return = ''; + for($n=$tags; $tags>0; $tags--) { + $return .= ''; + } + return $return; + } + + $styleA = ArrayHelper::merge($styleA, $style); + } + $styleString[] = array(); + foreach($styleA as $name => $content) { + if ($name === 'text-decoration') { + $content = implode(' ', $content); + } + $styleString[] = $name.':'.$content; + } + $tags++; + return 'language; + } + if ($sourceLanguage === null) { + $sourceLanguage = Yii::$app->sourceLanguage; + } + if ($language === $sourceLanguage) { + return $file; + } + $desiredFile = dirname($file) . DIRECTORY_SEPARATOR . $sourceLanguage . DIRECTORY_SEPARATOR . basename($file); + return is_file($desiredFile) ? $desiredFile : $file; + } + + /** + * Determines the MIME type of the specified file. + * This method will first try to determine the MIME type based on + * [finfo_open](http://php.net/manual/en/function.finfo-open.php). If this doesn't work, it will + * fall back to [[getMimeTypeByExtension()]]. + * @param string $file the file name. + * @param string $magicFile name of the optional magic database file, usually something like `/path/to/magic.mime`. + * This will be passed as the second parameter to [finfo_open](http://php.net/manual/en/function.finfo-open.php). + * @param boolean $checkExtension whether to use the file extension to determine the MIME type in case + * `finfo_open()` cannot determine it. + * @return string the MIME type (e.g. `text/plain`). Null is returned if the MIME type cannot be determined. + */ + public static function getMimeType($file, $magicFile = null, $checkExtension = true) + { + if (function_exists('finfo_open')) { + $info = finfo_open(FILEINFO_MIME_TYPE, $magicFile); + if ($info) { + $result = finfo_file($info, $file); + finfo_close($info); + if ($result !== false) { + return $result; + } + } + } + + return $checkExtension ? self::getMimeTypeByExtension($file) : null; + } + + /** + * Determines the MIME type based on the extension name of the specified file. + * This method will use a local map between extension names and MIME types. + * @param string $file the file name. + * @param string $magicFile the path of the file that contains all available MIME type information. + * If this is not set, the default file aliased by `@yii/util/mimeTypes.php` will be used. + * @return string the MIME type. Null is returned if the MIME type cannot be determined. + */ + public static function getMimeTypeByExtension($file, $magicFile = null) + { + static $mimeTypes = array(); + if ($magicFile === null) { + $magicFile = __DIR__ . '/mimeTypes.php'; + } + if (!isset($mimeTypes[$magicFile])) { + $mimeTypes[$magicFile] = require($magicFile); + } + if (($ext = pathinfo($file, PATHINFO_EXTENSION)) !== '') { + $ext = strtolower($ext); + if (isset($mimeTypes[$magicFile][$ext])) { + return $mimeTypes[$magicFile][$ext]; + } + } + return null; + } + + /** + * Copies a whole directory as another one. + * The files and sub-directories will also be copied over. + * @param string $src the source directory + * @param string $dst the destination directory + * @param array $options options for directory copy. Valid options are: + * + * - dirMode: integer, the permission to be set for newly copied directories. Defaults to 0777. + * - fileMode: integer, the permission to be set for newly copied files. Defaults to the current environment setting. + * - beforeCopy: callback, a PHP callback that is called before copying each sub-directory or file. + * If the callback returns false, the copy operation for the sub-directory or file will be cancelled. + * The signature of the callback should be: `function ($from, $to)`, where `$from` is the sub-directory or + * file to be copied from, while `$to` is the copy target. + * - afterCopy: callback, a PHP callback that is called after a sub-directory or file is successfully copied. + * The signature of the callback is similar to that of `beforeCopy`. + */ + public static function copyDirectory($src, $dst, $options = array()) + { + if (!is_dir($dst)) { + mkdir($dst, isset($options['dirMode']) ? $options['dirMode'] : 0777, true); + } + + $handle = opendir($src); + while (($file = readdir($handle)) !== false) { + if ($file === '.' || $file === '..') { + continue; + } + $from = $src . DIRECTORY_SEPARATOR . $file; + $to = $dst . DIRECTORY_SEPARATOR . $file; + if (!isset($options['beforeCopy']) || call_user_func($options['beforeCopy'], $from, $to)) { + if (is_file($from)) { + copy($from, $to); + if (isset($options['fileMode'])) { + @chmod($to, $options['fileMode']); + } + } else { + static::copyDirectory($from, $to, $options); + } + if (isset($options['afterCopy'])) { + call_user_func($options['afterCopy'], $from, $to); + } + } + } + closedir($handle); + } +} \ No newline at end of file diff --git a/framework/helpers/base/Html.php b/framework/helpers/base/Html.php new file mode 100644 index 0000000..bb1fed3 --- /dev/null +++ b/framework/helpers/base/Html.php @@ -0,0 +1,981 @@ + + * @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 (!empty($options['multiple']) && substr($name, -2) !== '[]') { + $name .= '[]'; + } + $options['name'] = $name; + if (isset($options['unselect'])) { + // add a hidden field so that if the list box has no option being selected, it still submits a value + if (substr($name, -2) === '[]') { + $name = substr($name, 0, -2); + } + $hidden = static::hiddenInput($name, $options['unselect']); + unset($options['unselect']); + } else { + $hidden = ''; + } + $selectOptions = static::renderSelectOptions($selection, $items, $options); + return $hidden . static::tag('select', "\n" . $selectOptions . "\n", $options); + } + + /** + * Generates a list of checkboxes. + * A checkbox list allows multiple selection, like [[listBox()]]. + * As a result, the corresponding submitted value is an array. + * @param string $name the name attribute of each checkbox. + * @param string|array $selection the selected value(s). + * @param array $items the data item used to generate the checkboxes. + * The array keys are the labels, while the array values are the corresponding checkbox values. + * Note that the labels will NOT be HTML-encoded, while the values will. + * @param array $options options (name => config) for the checkbox list. The following options are supported: + * + * - unselect: string, the value that should be submitted when none of the checkboxes is selected. + * By setting this option, a hidden input will be generated. + * - separator: string, the HTML code that separates items. + * - item: callable, a callback that can be used to customize the generation of the HTML code + * corresponding to a single item in $items. The signature of this callback must be: + * + * ~~~ + * function ($index, $label, $name, $checked, $value) + * ~~~ + * + * where $index is the zero-based index of the checkbox in the whole list; $label + * is the label for the checkbox; and $name, $value and $checked represent the name, + * value and the checked status of the checkbox input. + * @return string the generated checkbox list + */ + public static function checkboxList($name, $selection = null, $items = array(), $options = array()) + { + if (substr($name, -2) !== '[]') { + $name .= '[]'; + } + + $formatter = isset($options['item']) ? $options['item'] : null; + $lines = array(); + $index = 0; + foreach ($items as $value => $label) { + $checked = $selection !== null && + (!is_array($selection) && !strcmp($value, $selection) + || is_array($selection) && in_array($value, $selection)); + if ($formatter !== null) { + $lines[] = call_user_func($formatter, $index, $label, $name, $checked, $value); + } else { + $lines[] = static::label(static::checkbox($name, $checked, $value) . ' ' . $label); + } + $index++; + } + + if (isset($options['unselect'])) { + // add a hidden field so that if the list box has no option being selected, it still submits a value + $name2 = substr($name, -2) === '[]' ? substr($name, 0, -2) : $name; + $hidden = static::hiddenInput($name2, $options['unselect']); + } else { + $hidden = ''; + } + $separator = isset($options['separator']) ? $options['separator'] : "\n"; + + return $hidden . implode($separator, $lines); + } + + /** + * Generates a list of radio buttons. + * A radio button list is like a checkbox list, except that it only allows single selection. + * @param string $name the name attribute of each radio button. + * @param string|array $selection the selected value(s). + * @param array $items the data item used to generate the radio buttons. + * The array keys are the labels, while the array values are the corresponding radio button values. + * Note that the labels will NOT be HTML-encoded, while the values will. + * @param array $options options (name => config) for the radio button list. The following options are supported: + * + * - unselect: string, the value that should be submitted when none of the radio buttons is selected. + * By setting this option, a hidden input will be generated. + * - separator: string, the HTML code that separates items. + * - item: callable, a callback that can be used to customize the generation of the HTML code + * corresponding to a single item in $items. The signature of this callback must be: + * + * ~~~ + * function ($index, $label, $name, $checked, $value) + * ~~~ + * + * where $index is the zero-based index of the radio button in the whole list; $label + * is the label for the radio button; and $name, $value and $checked represent the name, + * value and the checked status of the radio button input. + * @return string the generated radio button list + */ + public static function radioList($name, $selection = null, $items = array(), $options = array()) + { + $formatter = isset($options['item']) ? $options['item'] : null; + $lines = array(); + $index = 0; + foreach ($items as $value => $label) { + $checked = $selection !== null && + (!is_array($selection) && !strcmp($value, $selection) + || is_array($selection) && in_array($value, $selection)); + if ($formatter !== null) { + $lines[] = call_user_func($formatter, $index, $label, $name, $checked, $value); + } else { + $lines[] = static::label(static::radio($name, $checked, $value) . ' ' . $label); + } + $index++; + } + + $separator = isset($options['separator']) ? $options['separator'] : "\n"; + if (isset($options['unselect'])) { + // add a hidden field so that if the list box has no option being selected, it still submits a value + $hidden = static::hiddenInput($name, $options['unselect']); + } else { + $hidden = ''; + } + + return $hidden . implode($separator, $lines); + } + + /** + * Renders the option tags that can be used by [[dropDownList()]] and [[listBox()]]. + * @param string|array $selection the selected value(s). This can be either a string for single selection + * or an array for multiple selections. + * @param array $items the option data items. The array keys are option values, and the array values + * are the corresponding option labels. The array can also be nested (i.e. some array values are arrays too). + * For each sub-array, an option group will be generated whose label is the key associated with the sub-array. + * If you have a list of data models, you may convert them into the format described above using + * [[\yii\helpers\ArrayHelper::map()]]. + * + * Note, the values and labels will be automatically HTML-encoded by this method, and the blank spaces in + * the labels will also be HTML-encoded. + * @param array $tagOptions the $options parameter that is passed to the [[dropDownList()]] or [[listBox()]] call. + * This method will take out these elements, if any: "prompt", "options" and "groups". See more details + * in [[dropDownList()]] for the explanation of these elements. + * + * @return string the generated list options + */ + public static function renderSelectOptions($selection, $items, &$tagOptions = array()) + { + $lines = array(); + if (isset($tagOptions['prompt'])) { + $prompt = str_replace(' ', ' ', static::encode($tagOptions['prompt'])); + $lines[] = static::tag('option', $prompt, array('value' => '')); + } + + $options = isset($tagOptions['options']) ? $tagOptions['options'] : array(); + $groups = isset($tagOptions['groups']) ? $tagOptions['groups'] : array(); + unset($tagOptions['prompt'], $tagOptions['options'], $tagOptions['groups']); + + foreach ($items as $key => $value) { + if (is_array($value)) { + $groupAttrs = isset($groups[$key]) ? $groups[$key] : array(); + $groupAttrs['label'] = $key; + $attrs = array('options' => $options, 'groups' => $groups); + $content = static::renderSelectOptions($selection, $value, $attrs); + $lines[] = static::tag('optgroup', "\n" . $content . "\n", $groupAttrs); + } else { + $attrs = isset($options[$key]) ? $options[$key] : array(); + $attrs['value'] = $key; + $attrs['selected'] = $selection !== null && + (!is_array($selection) && !strcmp($key, $selection) + || is_array($selection) && in_array($key, $selection)); + $lines[] = static::tag('option', str_replace(' ', ' ', static::encode($value)), $attrs); + } + } + + return implode("\n", $lines); + } + + /** + * Renders the HTML tag attributes. + * Boolean attributes such as s 'checked', 'disabled', 'readonly', will be handled specially + * according to [[booleanAttributes]] and [[showBooleanAttributeValues]]. + * @param array $attributes attributes to be rendered. The attribute values will be HTML-encoded using [[encode()]]. + * Attributes whose value is null will be ignored and not put in the rendering result. + * @return string the rendering result. If the attributes are not empty, they will be rendered + * into a string with a leading white space (such that it can be directly appended to the tag name + * in a tag. If there is no attribute, an empty string will be returned. + */ + public static function renderTagAttributes($attributes) + { + if (count($attributes) > 1) { + $sorted = array(); + foreach (static::$attributeOrder as $name) { + if (isset($attributes[$name])) { + $sorted[$name] = $attributes[$name]; + } + } + $attributes = array_merge($sorted, $attributes); + } + + $html = ''; + foreach ($attributes as $name => $value) { + if (isset(static::$booleanAttributes[strtolower($name)])) { + if ($value || strcasecmp($name, $value) === 0) { + $html .= static::$showBooleanAttributeValues ? " $name=\"$name\"" : " $name"; + } + } elseif ($value !== null) { + $html .= " $name=\"" . static::encode($value) . '"'; + } + } + return $html; + } + + /** + * Normalizes the input parameter to be a valid URL. + * + * If the input parameter + * + * - is an empty string: the currently requested URL will be returned; + * - is a non-empty string: it will be processed by [[Yii::getAlias()]] and returned; + * - is an array: the first array element is considered a route, while the rest of the name-value + * pairs are treated as the parameters to be used for URL creation using [[\yii\web\Controller::createUrl()]]. + * For example: `array('post/index', 'page' => 2)`, `array('index')`. + * + * @param array|string $url the parameter to be used to generate a valid URL + * @return string the normalized URL + * @throws InvalidParamException if the parameter is invalid. + */ + public static function url($url) + { + if (is_array($url)) { + if (isset($url[0])) { + $route = $url[0]; + $params = array_splice($url, 1); + if (Yii::$app->controller !== null) { + return Yii::$app->controller->createUrl($route, $params); + } else { + return Yii::$app->getUrlManager()->createUrl($route, $params); + } + } else { + throw new InvalidParamException('The array specifying a URL must contain at least one element.'); + } + } elseif ($url === '') { + return Yii::$app->getRequest()->getUrl(); + } else { + return Yii::getAlias($url); + } + } +} diff --git a/framework/helpers/base/SecurityHelper.php b/framework/helpers/base/SecurityHelper.php new file mode 100644 index 0000000..6ba48ba --- /dev/null +++ b/framework/helpers/base/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/helpers/base/StringHelper.php b/framework/helpers/base/StringHelper.php new file mode 100644 index 0000000..cb4b09b --- /dev/null +++ b/framework/helpers/base/StringHelper.php @@ -0,0 +1,125 @@ + + * @author Alex Makarov + * @since 2.0 + */ +class StringHelper +{ + /** + * Returns the number of bytes in the given string. + * This method ensures the string is treated as a byte array. + * It will use `mb_strlen()` if it is available. + * @param string $string the string being measured for length + * @return integer the number of bytes in the given string. + */ + public static function strlen($string) + { + return function_exists('mb_strlen') ? mb_strlen($string, '8bit') : strlen($string); + } + + /** + * Returns the portion of string specified by the start and length parameters. + * This method ensures the string is treated as a byte array. + * It will use `mb_substr()` if it is available. + * @param string $string the input string. Must be one character or longer. + * @param integer $start the starting position + * @param integer $length the desired portion length + * @return string the extracted part of string, or FALSE on failure or an empty string. + * @see http://www.php.net/manual/en/function.substr.php + */ + public static function substr($string, $start, $length) + { + return function_exists('mb_substr') ? mb_substr($string, $start, $length, '8bit') : substr($string, $start, $length); + } + + /** + * Converts a word to its plural form. + * Note that this is for English only! + * For example, 'apple' will become 'apples', and 'child' will become 'children'. + * @param string $name the word to be pluralized + * @return string the pluralized word + */ + public static function pluralize($name) + { + static $rules = array( + '/(m)ove$/i' => '\1oves', + '/(f)oot$/i' => '\1eet', + '/(c)hild$/i' => '\1hildren', + '/(h)uman$/i' => '\1umans', + '/(m)an$/i' => '\1en', + '/(s)taff$/i' => '\1taff', + '/(t)ooth$/i' => '\1eeth', + '/(p)erson$/i' => '\1eople', + '/([m|l])ouse$/i' => '\1ice', + '/(x|ch|ss|sh|us|as|is|os)$/i' => '\1es', + '/([^aeiouy]|qu)y$/i' => '\1ies', + '/(?:([^f])fe|([lr])f)$/i' => '\1\2ves', + '/(shea|lea|loa|thie)f$/i' => '\1ves', + '/([ti])um$/i' => '\1a', + '/(tomat|potat|ech|her|vet)o$/i' => '\1oes', + '/(bu)s$/i' => '\1ses', + '/(ax|test)is$/i' => '\1es', + '/s$/' => 's', + ); + foreach ($rules as $rule => $replacement) { + if (preg_match($rule, $name)) { + return preg_replace($rule, $replacement, $name); + } + } + return $name . 's'; + } + + /** + * Converts a CamelCase name into space-separated words. + * For example, 'PostTag' will be converted to 'Post Tag'. + * @param string $name the string to be converted + * @param boolean $ucwords whether to capitalize the first letter in each word + * @return string the resulting words + */ + public static function camel2words($name, $ucwords = true) + { + $label = trim(strtolower(str_replace(array('-', '_', '.'), ' ', preg_replace('/(? + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008-2011 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\helpers\base; + +/** + * 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(...)'; + } 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/helpers/mimeTypes.php b/framework/helpers/base/mimeTypes.php similarity index 100% rename from framework/helpers/mimeTypes.php rename to framework/helpers/base/mimeTypes.php diff --git a/framework/validators/CaptchaValidator.php b/framework/validators/CaptchaValidator.php index 3b4745b..ebb0039 100644 --- a/framework/validators/CaptchaValidator.php +++ b/framework/validators/CaptchaValidator.php @@ -68,6 +68,7 @@ class CaptchaValidator extends Validator /** * Returns the CAPTCHA action object. + * @throws InvalidConfigException * @return CaptchaAction the action object */ public function getCaptchaAction() diff --git a/framework/validators/FileValidator.php b/framework/validators/FileValidator.php index c104c05..b3de0b2 100644 --- a/framework/validators/FileValidator.php +++ b/framework/validators/FileValidator.php @@ -175,7 +175,7 @@ class FileValidator extends Validator if ($this->minSize !== null && $file->getSize() < $this->minSize) { $this->addError($object, $attribute, $this->tooSmall, array('{file}' => $file->getName(), '{limit}' => $this->minSize)); } - if (!empty($this->types) && !in_array(strtolower(FileHelper::getExtension($file->getName())), $this->types, true)) { + if (!empty($this->types) && !in_array(strtolower(pathinfo($file->getName(), PATHINFO_EXTENSION)), $this->types, true)) { $this->addError($object, $attribute, $this->wrongType, array('{file}' => $file->getName(), '{extensions}' => implode(', ', $this->types))); } break; diff --git a/framework/validators/UniqueValidator.php b/framework/validators/UniqueValidator.php index fa55df7..2240e0a 100644 --- a/framework/validators/UniqueValidator.php +++ b/framework/validators/UniqueValidator.php @@ -60,7 +60,7 @@ class UniqueValidator extends Validator } /** @var $className \yii\db\ActiveRecord */ - $className = $this->className === null ? get_class($object) : \Yii::import($this->className); + $className = $this->className === null ? get_class($object) : Yii::import($this->className); $attributeName = $this->attributeName === null ? $attribute : $this->attributeName; $table = $className::getTableSchema(); diff --git a/framework/views/error.php b/framework/views/error.php index 893640a..548d04b 100644 --- a/framework/views/error.php +++ b/framework/views/error.php @@ -4,7 +4,7 @@ * @var \yii\base\ErrorHandler $context */ $context = $this->context; -$title = $context->htmlEncode($exception instanceof \yii\base\Exception || $exception instanceof \yii\base\ErrorException ? $exception->getName() : get_class($exception)); +$title = $context->htmlEncode($exception instanceof \yii\base\Exception ? $exception->getName() : get_class($exception)); ?> diff --git a/framework/views/exception.php b/framework/views/exception.php index db29302..f2aced0 100644 --- a/framework/views/exception.php +++ b/framework/views/exception.php @@ -4,7 +4,7 @@ * @var \yii\base\ErrorHandler $context */ $context = $this->context; -$title = $context->htmlEncode($exception instanceof \yii\base\Exception || $exception instanceof \yii\base\ErrorException ? $exception->getName().' ('.get_class($exception).')' : get_class($exception)); +$title = $context->htmlEncode($exception instanceof \yii\base\Exception ? $exception->getName().' ('.get_class($exception).')' : get_class($exception)); ?> diff --git a/framework/web/Application.php b/framework/web/Application.php index f9b615d..3387044 100644 --- a/framework/web/Application.php +++ b/framework/web/Application.php @@ -98,6 +98,15 @@ class Application extends \yii\base\Application } /** + * Returns the asset manager. + * @return AssetManager the asset manager component + */ + public function getAssetManager() + { + return $this->getComponent('assetManager'); + } + + /** * Registers the core application components. * @see setComponents */ @@ -117,6 +126,9 @@ class Application extends \yii\base\Application 'user' => array( 'class' => 'yii\web\User', ), + 'assetManager' => array( + 'class' => 'yii\web\AssetManager', + ), )); } } diff --git a/framework/web/AssetBundle.php b/framework/web/AssetBundle.php new file mode 100644 index 0000000..4e1eb59 --- /dev/null +++ b/framework/web/AssetBundle.php @@ -0,0 +1,176 @@ + + * @since 2.0 + */ +class AssetBundle extends Object +{ + /** + * @var string the root directory of the source asset files. A source asset file + * is a file that is part of your source code repository of your Web application. + * + * You must set this property if the directory containing the source asset files + * is not Web accessible (this is usually the case for extensions). + * + * By setting this property, the asset manager will publish the source asset files + * to a Web-accessible directory [[basePath]]. + * + * You can use either a directory or an alias of the directory. + */ + public $sourcePath; + /** + * @var string the Web-accessible directory that contains the asset files in this bundle. + * + * If [[sourcePath]] is set, this property will be *overwritten* by [[AssetManager]] + * when it publishes the asset files from [[sourcePath]]. + * + * If the bundle contains any assets that are specified in terms of relative file path, + * then this property must be set either manually or automatically (by asset manager via + * asset publishing). + * + * You can use either a directory or an alias of the directory. + */ + public $basePath; + /** + * @var string the base URL that will be prefixed to the asset files for them to + * be accessed via Web server. + * + * If [[sourcePath]] is set, this property will be *overwritten* by [[AssetManager]] + * when it publishes the asset files from [[sourcePath]]. + * + * If the bundle contains any assets that are specified in terms of relative file path, + * then this property must be set either manually or automatically (by asset manager via + * asset publishing). + * + * You can use either a URL or an alias of the URL. + */ + public $baseUrl; + /** + * @var array list of the bundle names that this bundle depends on + */ + public $depends = array(); + /** + * @var array list of JavaScript files that this bundle contains. Each JavaScript file can + * be either a file path (without leading slash) relative to [[basePath]] or a URL representing + * an external JavaScript file. + * + * Note that only forward slash "/" can be used as directory separator. + */ + public $js = array(); + /** + * @var array list of CSS files that this bundle contains. Each CSS file can + * be either a file path (without leading slash) relative to [[basePath]] or a URL representing + * an external CSS file. + * + * Note that only forward slash "/" can be used as directory separator. + */ + public $css = array(); + /** + * @var array the options that will be passed to [[\yii\base\View::registerJsFile()]] + * when registering the JS files in this bundle. + */ + public $jsOptions = array(); + /** + * @var array the options that will be passed to [[\yii\base\View::registerCssFile()]] + * when registering the CSS files in this bundle. + */ + public $cssOptions = array(); + /** + * @var array the options to be passed to [[AssetManager::publish()]] when the asset bundle + * is being published. + */ + public $publishOptions = array(); + + /** + * Initializes the bundle. + */ + public function init() + { + if ($this->sourcePath !== null) { + $this->sourcePath = rtrim(Yii::getAlias($this->sourcePath), '/\\'); + } + if ($this->basePath !== null) { + $this->basePath = rtrim(Yii::getAlias($this->basePath), '/\\'); + } + if ($this->baseUrl !== null) { + $this->baseUrl = rtrim(Yii::getAlias($this->baseUrl), '/'); + } + } + + /** + * Registers the CSS and JS files with the given view. + * This method will first register all dependent asset bundles. + * It will then try to convert non-CSS or JS files (e.g. LESS, Sass) into the corresponding + * CSS or JS files using [[AssetManager::converter|asset converter]]. + * @param \yii\base\View $view the view that the asset files to be registered with. + * @throws InvalidConfigException if [[baseUrl]] or [[basePath]] is not set when the bundle + * contains internal CSS or JS files. + */ + public function registerAssets($view) + { + foreach ($this->depends as $name) { + $view->registerAssetBundle($name); + } + + $this->publish($view->getAssetManager()); + + foreach ($this->js as $js) { + $view->registerJsFile($js, $this->jsOptions); + } + foreach ($this->css as $css) { + $view->registerCssFile($css, $this->cssOptions); + } + } + + /** + * Publishes the asset bundle if its source code is not under Web-accessible directory. + * @param AssetManager $am the asset manager to perform the asset publishing + * @throws InvalidConfigException if [[baseUrl]] or [[basePath]] is not set when the bundle + * contains internal CSS or JS files. + */ + public function publish($am) + { + if ($this->sourcePath !== null) { + list ($this->basePath, $this->baseUrl) = $am->publish($this->sourcePath, $this->publishOptions); + } + $converter = $am->getConverter(); + foreach ($this->js as $i => $js) { + if (strpos($js, '/') !== 0 && strpos($js, '://') === false) { + if (isset($this->basePath, $this->baseUrl)) { + $this->js[$i] = $converter->convert($js, $this->basePath, $this->baseUrl); + } else { + throw new InvalidConfigException('Both of the "baseUrl" and "basePath" properties must be set.'); + } + } + } + foreach ($this->css as $i => $css) { + if (strpos($css, '/') !== 0 && strpos($css, '://') === false) { + if (isset($this->basePath, $this->baseUrl)) { + $this->css[$i] = $converter->convert($css, $this->basePath, $this->baseUrl); + } else { + throw new InvalidConfigException('Both of the "baseUrl" and "basePath" properties must be set.'); + } + } + } + } +} \ No newline at end of file diff --git a/framework/web/AssetConverter.php b/framework/web/AssetConverter.php new file mode 100644 index 0000000..8340be5 --- /dev/null +++ b/framework/web/AssetConverter.php @@ -0,0 +1,62 @@ + + * @since 2.0 + */ +class AssetConverter extends Component implements IAssetConverter +{ + /** + * @var array the commands that are used to perform the asset conversion. + * The keys are the asset file extension names, and the values are the corresponding + * target script types (either "css" or "js") and the commands used for the conversion. + */ + public $commands = array( + 'less' => array('css', 'lessc {from} {to}'), + 'scss' => array('css', 'sass {from} {to}'), + 'sass' => array('css', 'sass {from} {to}'), + 'styl' => array('js', 'stylus < {from} > {to}'), + ); + + /** + * Converts a given asset file into a CSS or JS file. + * @param string $asset the asset file path, relative to $basePath + * @param string $basePath the directory the $asset is relative to. + * @param string $baseUrl the URL corresponding to $basePath + * @return string the URL to the converted asset file. + */ + public function convert($asset, $basePath, $baseUrl) + { + $pos = strrpos($asset, '.'); + if ($pos !== false) { + $ext = substr($asset, $pos + 1); + if (isset($this->commands[$ext])) { + list ($ext, $command) = $this->commands[$ext]; + $result = substr($asset, 0, $pos + 1) . $ext; + if (@filemtime("$basePath/$result") < filemtime("$basePath/$asset")) { + $output = array(); + $command = strtr($command, array( + '{from}' => "$basePath/$asset", + '{to}' => "$basePath/$result", + )); + exec($command, $output); + Yii::info("Converted $asset into $result: " . implode("\n", $output), __METHOD__); + return "$baseUrl/$result"; + } + } + } + return "$baseUrl/$asset"; + } +} \ No newline at end of file diff --git a/framework/web/AssetManager.php b/framework/web/AssetManager.php index 60f4c07..95dcbd2 100644 --- a/framework/web/AssetManager.php +++ b/framework/web/AssetManager.php @@ -1,153 +1,188 @@ * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008-2011 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ +namespace yii\web; + +use Yii; +use yii\base\Component; +use yii\base\InvalidConfigException; +use yii\base\InvalidParamException; +use yii\helpers\FileHelper; /** - * CAssetManager is a Web application component that manages private files (called assets) and makes them accessible by Web clients. - * - * It achieves this goal by copying assets to a Web-accessible directory - * and returns the corresponding URL for accessing them. - * - * To publish an asset, simply call {@link publish()}. - * - * The Web-accessible directory holding the published files is specified - * by {@link setBasePath basePath}, which defaults to the "assets" directory - * under the directory containing the application entry script file. - * The property {@link setBaseUrl baseUrl} refers to the URL for accessing - * the {@link setBasePath basePath}. - * - * @property string $basePath The root directory storing the published asset files. Defaults to 'WebRoot/assets'. - * @property string $baseUrl The base url that the published asset files can be accessed. - * Note, the ending slashes are stripped off. Defaults to '/AppBaseUrl/assets'. + * AssetManager manages asset bundles and asset publishing. * * @author Qiang Xue - * @version $Id$ - * @package system.web - * @since 1.0 + * @since 2.0 */ -class CAssetManager extends CApplicationComponent +class AssetManager extends Component { /** - * Default web accessible base path for storing private files + * @var array list of available asset bundles. The keys are the bundle names, and the values are the configuration + * arrays for creating the [[AssetBundle]] objects. + */ + public $bundles; + /** + * @return string the root directory storing the published asset files. */ - const DEFAULT_BASEPATH='assets'; + public $basePath = '@wwwroot/assets'; + /** + * @return string the base URL through which the published asset files can be accessed. + */ + public $baseUrl = '@www/assets'; /** * @var boolean whether to use symbolic link to publish asset files. Defaults to false, meaning - * asset files are copied to public folders. Using symbolic links has the benefit that the published - * assets will always be consistent with the source assets. This is especially useful during development. + * asset files are copied to [[basePath]]. Using symbolic links has the benefit that the published + * assets will always be consistent with the source assets and there is no copy operation required. + * This is especially useful during development. * * However, there are special requirements for hosting environments in order to use symbolic links. * In particular, symbolic links are supported only on Linux/Unix, and Windows Vista/2008 or greater. - * The latter requires PHP 5.3 or greater. * * Moreover, some Web servers need to be properly configured so that the linked assets are accessible * to Web users. For example, for Apache Web server, the following configuration directive should be added * for the Web folder: - *
-	 * Options FollowSymLinks
-	 * 
* - * @since 1.1.5 + * ~~~ + * Options FollowSymLinks + * ~~~ */ - public $linkAssets=false; + public $linkAssets = false; /** - * @var array list of directories and files which should be excluded from the publishing process. - * Defaults to exclude '.svn' and '.gitignore' files only. This option has no effect if {@link linkAssets} is enabled. - * @since 1.1.6 - **/ - public $excludeFiles=array('.svn','.gitignore'); - /** - * @var integer the permission to be set for newly generated asset files. - * This value will be used by PHP chmod function. - * Defaults to 0666, meaning the file is read-writable by all users. - * @since 1.1.8 + * @var integer the permission to be set for newly published asset files. + * This value will be used by PHP chmod() function. + * If not set, the permission will be determined by the current environment. */ - public $newFileMode=0666; + public $fileMode; /** * @var integer the permission to be set for newly generated asset directories. - * This value will be used by PHP chmod function. + * This value will be used by PHP chmod() function. * Defaults to 0777, meaning the directory can be read, written and executed by all users. - * @since 1.1.8 - */ - public $newDirMode=0777; - /** - * @var string base web accessible path for storing private files */ - private $_basePath; - /** - * @var string base URL for accessing the publishing directory. - */ - private $_baseUrl; - /** - * @var array published assets - */ - private $_published=array(); + public $dirMode = 0777; /** - * @return string the root directory storing the published asset files. Defaults to 'WebRoot/assets'. + * Initializes the component. + * @throws InvalidConfigException if [[basePath]] is invalid */ - public function getBasePath() + public function init() { - if($this->_basePath===null) - { - $request=\Yii::$app->getRequest(); - $this->setBasePath(dirname($request->getScriptFile()).DIRECTORY_SEPARATOR.self::DEFAULT_BASEPATH); + parent::init(); + $this->basePath = Yii::getAlias($this->basePath); + if (!is_dir($this->basePath)) { + throw new InvalidConfigException("The directory does not exist: {$this->basePath}"); + } elseif (!is_writable($this->basePath)) { + throw new InvalidConfigException("The directory is not writable by the Web process: {$this->basePath}"); + } else { + $this->basePath = realpath($this->basePath); + } + $this->baseUrl = rtrim(Yii::getAlias($this->baseUrl), '/'); + + foreach (require(YII_PATH . '/assets.php') as $name => $bundle) { + if (!isset($this->bundles[$name])) { + $this->bundles[$name] = $bundle; + } } - return $this->_basePath; } /** - * Sets the root directory storing published asset files. - * @param string $value the root directory storing published asset files - * @throws CException if the base path is invalid + * Returns the named bundle. + * This method will first look for the bundle in [[bundles]]. If not found, + * it will attempt to find the bundle from an installed extension using the following procedure: + * + * 1. Convert the bundle into a path alias; + * 2. Determine the root alias and use it to locate the bundle manifest file "assets.php"; + * 3. Look for the bundle in the manifest file. + * + * For example, given the bundle name "foo/button", the method will first convert it + * into the path alias "@foo/button"; since "@foo" is the root alias, it will look + * for the bundle manifest file "@foo/assets.php". The manifest file should return an array + * that lists the bundles used by the "foo/button" extension. The array format is the same as [[bundles]]. + * + * @param string $name the bundle name + * @return AssetBundle the loaded bundle object. Null is returned if the bundle does not exist. */ - public function setBasePath($value) + public function getBundle($name) { - if(($basePath=realpath($value))!==false && is_dir($basePath) && is_writable($basePath)) - $this->_basePath=$basePath; - else - throw new CException(Yii::t('yii|CAssetManager.basePath "{path}" is invalid. Please make sure the directory exists and is writable by the Web server process.', - array('{path}'=>$value))); + if (!isset($this->bundles[$name])) { + $rootAlias = Yii::getRootAlias("@$name"); + if ($rootAlias !== false) { + $manifest = Yii::getAlias("$rootAlias/assets.php", false); + if ($manifest !== false && is_file($manifest)) { + foreach (require($manifest) as $bn => $config) { + $this->bundles[$bn] = $config; + } + } + } + if (!isset($this->bundles[$name])) { + return null; + } + } + if (is_array($this->bundles[$name])) { + $config = $this->bundles[$name]; + if (!isset($config['class'])) { + $config['class'] = 'yii\\web\\AssetBundle'; + $this->bundles[$name] = Yii::createObject($config); + } + } + + return $this->bundles[$name]; } + private $_converter; + /** - * @return string the base url that the published asset files can be accessed. - * Note, the ending slashes are stripped off. Defaults to '/AppBaseUrl/assets'. + * Returns the asset converter. + * @return IAssetConverter the asset converter. */ - public function getBaseUrl() + public function getConverter() { - if($this->_baseUrl===null) - { - $request=\Yii::$app->getRequest(); - $this->setBaseUrl($request->getBaseUrl().'/'.self::DEFAULT_BASEPATH); + if ($this->_converter === null) { + $this->_converter = Yii::createObject(array( + 'class' => 'yii\\web\\AssetConverter', + )); + } elseif (is_array($this->_converter) || is_string($this->_converter)) { + $this->_converter = Yii::createObject($this->_converter); } - return $this->_baseUrl; + return $this->_converter; } /** - * @param string $value the base url that the published asset files can be accessed + * Sets the asset converter. + * @param array|IAssetConverter $value the asset converter. This can be either + * an object implementing the [[IAssetConverter]] interface, or a configuration + * array that can be used to create the asset converter object. */ - public function setBaseUrl($value) + public function setConverter($value) { - $this->_baseUrl=rtrim($value,'/'); + $this->_converter = $value; } /** + * @var array published assets + */ + private $_published = array(); + + /** * Publishes a file or a directory. - * This method will copy the specified asset to a web accessible directory - * and return the URL for accessing the published asset. - *
    - *
  • If the asset is a file, its file modification time will be checked - * to avoid unnecessary file copying;
  • - *
  • If the asset is a directory, all files and subdirectories under it will - * be published recursively. Note, in case $forceCopy is false the method only checks the - * existence of the target directory to avoid repetitive copying.
  • - *
+ * + * This method will copy the specified file or directory to [[basePath]] so that + * it can be accessed via the Web server. + * + * If the asset is a file, its file modification time will be checked to avoid + * unnecessary file copying. + * + * If the asset is a directory, all files and subdirectories under it will be published recursively. + * Note, in case $forceCopy is false the method only checks the existence of the target + * directory to avoid repetitive copying (which is very expensive). + * + * By default, when publishing a directory, subdirectories and files whose name starts with a dot "." + * will NOT be published. If you want to change this behavior, you may specify the "beforeCopy" option + * as explained in the `$options` parameter. * * Note: On rare scenario, a race condition can develop that will lead to a * one-time-manifestation of a non-critical problem in the creation of the directory @@ -157,85 +192,85 @@ class CAssetManager extends CApplicationComponent * discussion: http://code.google.com/p/yii/issues/detail?id=2579 * * @param string $path the asset (file or directory) to be published - * @param boolean $hashByName whether the published directory should be named as the hashed basename. - * If false, the name will be the hash taken from dirname of the path being published and path mtime. - * Defaults to false. Set true if the path being published is shared among - * different extensions. - * @param integer $level level of recursive copying when the asset is a directory. - * Level -1 means publishing all subdirectories and files; - * Level 0 means publishing only the files DIRECTLY under the directory; - * level N means copying those directories that are within N levels. - * @param boolean $forceCopy whether we should copy the asset file or directory even if it is already published before. - * This parameter is set true mainly during development stage when the original - * assets are being constantly changed. The consequence is that the performance - * is degraded, which is not a concern during development, however. - * This parameter has been available since version 1.1.2. - * @return string an absolute URL to the published asset - * @throws CException if the asset to be published does not exist. + * @param array $options the options to be applied when publishing a directory. + * The following options are supported: + * + * - beforeCopy: callback, a PHP callback that is called before copying each sub-directory or file. + * This option is used only when publishing a directory. If the callback returns false, the copy + * operation for the sub-directory or file will be cancelled. + * The signature of the callback should be: `function ($from, $to)`, where `$from` is the sub-directory or + * file to be copied from, while `$to` is the copy target. + * - afterCopy: callback, a PHP callback that is called after a sub-directory or file is successfully copied. + * This option is used only when publishing a directory. The signature of the callback is similar to that + * of `beforeCopy`. + * - forceCopy: boolean, whether the directory being published should be copied even if + * it is found in the target directory. This option is used only when publishing a directory. + * You may want to set this to be true during the development stage to make sure the published + * directory is always up-to-date. Do not set this to true on production servers as it will + * significantly degrade the performance. + * @return array the path (directory or file path) and the URL that the asset is published as. + * @throws InvalidParamException if the asset to be published does not exist. */ - public function publish($path,$hashByName=false,$level=-1,$forceCopy=false) + public function publish($path, $options = array()) { - if(isset($this->_published[$path])) + if (isset($this->_published[$path])) { return $this->_published[$path]; - else if(($src=realpath($path))!==false) - { - if(is_file($src)) - { - $dir=$this->hash($hashByName ? basename($src) : dirname($src).filemtime($src)); - $fileName=basename($src); - $dstDir=$this->getBasePath().DIRECTORY_SEPARATOR.$dir; - $dstFile=$dstDir.DIRECTORY_SEPARATOR.$fileName; + } - if($this->linkAssets) - { - if(!is_file($dstFile)) - { - if(!is_dir($dstDir)) - { - mkdir($dstDir); - @chmod($dstDir, $this->newDirMode); - } - symlink($src,$dstFile); - } - } - else if(@filemtime($dstFile)<@filemtime($src)) - { - if(!is_dir($dstDir)) - { - mkdir($dstDir); - @chmod($dstDir, $this->newDirMode); - } - copy($src,$dstFile); - @chmod($dstFile, $this->newFileMode); - } + $src = realpath($path); + if ($src === false) { + throw new InvalidParamException("The file or directory to be published does not exist: $path"); + } - return $this->_published[$path]=$this->getBaseUrl()."/$dir/$fileName"; + if (is_file($src)) { + $dir = $this->hash(dirname($src) . filemtime($src)); + $fileName = basename($src); + $dstDir = $this->basePath . DIRECTORY_SEPARATOR . $dir; + $dstFile = $dstDir . DIRECTORY_SEPARATOR . $fileName; + + if (!is_dir($dstDir)) { + mkdir($dstDir, $this->dirMode, true); } - else if(is_dir($src)) - { - $dir=$this->hash($hashByName ? basename($src) : $src.filemtime($src)); - $dstDir=$this->getBasePath().DIRECTORY_SEPARATOR.$dir; - if($this->linkAssets) - { - if(!is_dir($dstDir)) - symlink($src,$dstDir); + if ($this->linkAssets) { + if (!is_file($dstFile)) { + symlink($src, $dstFile); } - else if(!is_dir($dstDir) || $forceCopy) - { - CFileHelper::copyDirectory($src,$dstDir,array( - 'exclude'=>$this->excludeFiles, - 'level'=>$level, - 'newDirMode'=>$this->newDirMode, - 'newFileMode'=>$this->newFileMode, - )); + } elseif (@filemtime($dstFile) < @filemtime($src)) { + copy($src, $dstFile); + if ($this->fileMode !== null) { + @chmod($dstFile, $this->fileMode); } + } - return $this->_published[$path]=$this->getBaseUrl().'/'.$dir; + return $this->_published[$path] = array($dstFile, $this->baseUrl . "/$dir/$fileName"); + } else { + $dir = $this->hash($src . filemtime($src)); + $dstDir = $this->basePath . DIRECTORY_SEPARATOR . $dir; + if ($this->linkAssets) { + if (!is_dir($dstDir)) { + symlink($src, $dstDir); + } + } elseif (!is_dir($dstDir) || !empty($options['forceCopy'])) { + $opts = array( + 'dirMode' => $this->dirMode, + 'fileMode' => $this->fileMode, + ); + if (isset($options['beforeCopy'])) { + $opts['beforeCopy'] = $options['beforeCopy']; + } else { + $opts['beforeCopy'] = function ($from, $to) { + return strncmp(basename($from), '.', 1) !== 0; + }; + } + if (isset($options['afterCopy'])) { + $opts['afterCopy'] = $options['afterCopy']; + } + FileHelper::copyDirectory($src, $dstDir, $opts); } + + return $this->_published[$path] = array($dstDir, $this->baseUrl . '/' . $dir); } - throw new CException(Yii::t('yii|The asset "{asset}" to be published does not exist.', - array('{asset}'=>$path))); } /** @@ -243,24 +278,20 @@ class CAssetManager extends CApplicationComponent * This method does not perform any publishing. It merely tells you * if the file or directory is published, where it will go. * @param string $path directory or file path being published - * @param boolean $hashByName whether the published directory should be named as the hashed basename. - * If false, the name will be the hash taken from dirname of the path being published and path mtime. - * Defaults to false. Set true if the path being published is shared among - * different extensions. * @return string the published file path. False if the file or directory does not exist */ - public function getPublishedPath($path,$hashByName=false) + public function getPublishedPath($path) { - if(($path=realpath($path))!==false) - { - $base=$this->getBasePath().DIRECTORY_SEPARATOR; - if(is_file($path)) - return $base . $this->hash($hashByName ? basename($path) : dirname($path).filemtime($path)) . DIRECTORY_SEPARATOR . basename($path); - else - return $base . $this->hash($hashByName ? basename($path) : $path.filemtime($path)); - } - else + if (($path = realpath($path)) !== false) { + $base = $this->basePath . DIRECTORY_SEPARATOR; + if (is_file($path)) { + return $base . $this->hash(dirname($path) . filemtime($path)) . DIRECTORY_SEPARATOR . basename($path); + } else { + return $base . $this->hash($path . filemtime($path)); + } + } else { return false; + } } /** @@ -268,25 +299,22 @@ class CAssetManager extends CApplicationComponent * This method does not perform any publishing. It merely tells you * if the file path is published, what the URL will be to access it. * @param string $path directory or file path being published - * @param boolean $hashByName whether the published directory should be named as the hashed basename. - * If false, the name will be the hash taken from dirname of the path being published and path mtime. - * Defaults to false. Set true if the path being published is shared among - * different extensions. * @return string the published URL for the file or directory. False if the file or directory does not exist. */ - public function getPublishedUrl($path,$hashByName=false) + public function getPublishedUrl($path) { - if(isset($this->_published[$path])) + if (isset($this->_published[$path])) { return $this->_published[$path]; - if(($path=realpath($path))!==false) - { - if(is_file($path)) - return $this->getBaseUrl().'/'.$this->hash($hashByName ? basename($path) : dirname($path).filemtime($path)).'/'.basename($path); - else - return $this->getBaseUrl().'/'.$this->hash($hashByName ? basename($path) : $path.filemtime($path)); } - else + if (($path = realpath($path)) !== false) { + if (is_file($path)) { + return $this->baseUrl . '/' . $this->hash(dirname($path) . filemtime($path)) . '/' . basename($path); + } else { + return $this->baseUrl . '/' . $this->hash($path . filemtime($path)); + } + } else { return false; + } } /** @@ -297,6 +325,6 @@ class CAssetManager extends CApplicationComponent */ protected function hash($path) { - return sprintf('%x',crc32($path.Yii::getVersion())); + return sprintf('%x', crc32($path . Yii::getVersion())); } } diff --git a/framework/web/IAssetConverter.php b/framework/web/IAssetConverter.php new file mode 100644 index 0000000..4334d3e --- /dev/null +++ b/framework/web/IAssetConverter.php @@ -0,0 +1,27 @@ + + * @since 2.0 + */ +interface IAssetConverter +{ + /** + * Converts a given asset file into a CSS or JS file. + * @param string $asset the asset file path, relative to $basePath + * @param string $basePath the directory the $asset is relative to. + * @param string $baseUrl the URL corresponding to $basePath + * @return string the URL to the converted asset file. If the given asset does not + * need conversion, "$baseUrl/$asset" should be returned. + */ + public function convert($asset, $basePath, $baseUrl); +} \ No newline at end of file diff --git a/framework/web/Response.php b/framework/web/Response.php index da2482f..1d604e9 100644 --- a/framework/web/Response.php +++ b/framework/web/Response.php @@ -112,8 +112,8 @@ class Response extends \yii\base\Response *
  • mimeType: mime type of the file, if not set it will be guessed automatically based on the file name, if set to null no content-type header will be sent.
  • *
  • xHeader: appropriate x-sendfile header, defaults to "X-Sendfile"
  • *
  • terminate: whether to terminate the current application after calling this method, defaults to true
  • - *
  • forceDownload: specifies whether the file will be downloaded or shown inline, defaults to true. (Since version 1.1.9.)
  • - *
  • addHeaders: an array of additional http headers in header-value pairs (available since version 1.1.10)
  • + *
  • forceDownload: specifies whether the file will be downloaded or shown inline, defaults to true
  • + *
  • addHeaders: an array of additional http headers in header-value pairs
  • * */ public function xSendFile($filePath, $options = array()) diff --git a/framework/web/Sort.php b/framework/web/Sort.php index 7cfeeca..e5c2451 100644 --- a/framework/web/Sort.php +++ b/framework/web/Sort.php @@ -216,7 +216,7 @@ class Sort extends \yii\base\Object $url = $this->createUrl($attribute); - return Html::link($label, $url, $htmlOptions); + return Html::a($label, $url, $htmlOptions); } private $_attributeOrders; diff --git a/framework/web/UrlManager.php b/framework/web/UrlManager.php index 459e8e8..755d644 100644 --- a/framework/web/UrlManager.php +++ b/framework/web/UrlManager.php @@ -74,9 +74,6 @@ class UrlManager extends Component public function init() { parent::init(); - if (is_string($this->cache)) { - $this->cache = Yii::$app->getComponent($this->cache); - } $this->compileRules(); } @@ -88,6 +85,9 @@ class UrlManager extends Component if (!$this->enablePrettyUrl || $this->rules === array()) { return; } + if (is_string($this->cache)) { + $this->cache = Yii::$app->getComponent($this->cache); + } if ($this->cache instanceof Cache) { $key = $this->cache->buildKey(__CLASS__); $hash = md5(json_encode($this->rules)); @@ -104,7 +104,7 @@ class UrlManager extends Component $this->rules[$i] = Yii::createObject($rule); } - if ($this->cache instanceof Cache) { + if (isset($key, $hash)) { $this->cache->set($key, array($this->rules, $hash)); } } diff --git a/framework/web/User.php b/framework/web/User.php index 4dc2607..435b606 100644 --- a/framework/web/User.php +++ b/framework/web/User.php @@ -32,7 +32,7 @@ class User extends Component const EVENT_AFTER_LOGOUT = 'afterLogout'; /** - * @var string the class name of the [[identity]] object. + * @var string the class name or alias of the [[identity]] object. */ public $identityClass; /** @@ -131,7 +131,7 @@ class User extends Component $this->_identity = null; } else { /** @var $class Identity */ - $class = $this->identityClass; + $class = Yii::import($this->identityClass); $this->_identity = $class::findIdentity($id); } } diff --git a/framework/widgets/ActiveForm.php b/framework/widgets/ActiveForm.php index 48bc181..83506dd 100644 --- a/framework/widgets/ActiveForm.php +++ b/framework/widgets/ActiveForm.php @@ -39,10 +39,16 @@ class ActiveForm extends Widget public $errorMessageClass = 'yii-error-message'; /** * @var string the default CSS class that indicates an input has error. - * This is */ public $errorClass = 'yii-error'; + /** + * @var string the default CSS class that indicates an input validated successfully. + */ public $successClass = 'yii-success'; + + /** + * @var string the default CSS class that indicates an input is currently being validated. + */ public $validatingClass = 'yii-validating'; /** * @var boolean whether to enable client-side data validation. Defaults to false. @@ -64,7 +70,7 @@ class ActiveForm extends Widget $models = array($models); } - $showAll = isset($options['showAll']) && $options['showAll']; + $showAll = !empty($options['showAll']); $lines = array(); /** @var $model Model */ foreach ($models as $model) { @@ -127,6 +133,14 @@ class ActiveForm extends Widget return Html::label($label, $for, $options); } + /** + * @param string $type + * @param Model $model + * @param string $attribute + * @param array $options + * + * @return string + */ public function input($type, $model, $attribute, $options = array()) { $value = $this->getAttributeValue($model, $attribute); diff --git a/framework/widgets/Clip.php b/framework/widgets/Block.php similarity index 54% rename from framework/widgets/Clip.php rename to framework/widgets/Block.php index f321209..d6f7317 100644 --- a/framework/widgets/Clip.php +++ b/framework/widgets/Block.php @@ -7,28 +7,26 @@ namespace yii\widgets; -use Yii; use yii\base\Widget; -use yii\base\View; /** * @author Qiang Xue * @since 2.0 */ -class Clip extends Widget +class Block extends Widget { /** - * @var string the ID of this clip. + * @var string the ID of this block. */ public $id; /** - * @var boolean whether to render the clip content in place. Defaults to false, - * meaning the captured clip will not be displayed. + * @var boolean whether to render the block content in place. Defaults to false, + * meaning the captured block content will not be displayed. */ public $renderInPlace = false; /** - * Starts recording a clip. + * Starts recording a block. */ public function init() { @@ -37,15 +35,15 @@ class Clip extends Widget } /** - * Ends recording a clip. - * This method stops output buffering and saves the rendering result as a named clip in the controller. + * Ends recording a block. + * This method stops output buffering and saves the rendering result as a named block in the controller. */ public function run() { - $clip = ob_get_clean(); - if ($this->renderClip) { - echo $clip; + $block = ob_get_clean(); + if ($this->renderInPlace) { + echo $block; } - $this->view->clips[$this->id] = $clip; + $this->view->blocks[$this->id] = $block; } } \ No newline at end of file diff --git a/framework/yiic.php b/framework/yiic.php index 0db69bb..3872e2f 100644 --- a/framework/yiic.php +++ b/framework/yiic.php @@ -14,10 +14,9 @@ defined('STDIN') or define('STDIN', fopen('php://stdin', 'r')); require(__DIR__ . '/yii.php'); -$id = 'yiic'; -$basePath = __DIR__ . '/console'; - -$application = new yii\console\Application($id, $basePath, array( +$application = new yii\console\Application(array( + 'id' => 'yiic', + 'basePath' => __DIR__ . '/console', 'controllerPath' => '@yii/console/controllers', )); $application->run(); diff --git a/tests/unit/bootstrap.php b/tests/unit/bootstrap.php index 4a388c6..8290bbe 100644 --- a/tests/unit/bootstrap.php +++ b/tests/unit/bootstrap.php @@ -9,4 +9,6 @@ require_once(__DIR__ . '/../../framework/yii.php'); Yii::setAlias('@yiiunit', __DIR__); +new \yii\web\Application(array('id' => 'testapp', 'basePath' => __DIR__)); + require_once(__DIR__ . '/TestCase.php'); diff --git a/tests/unit/framework/YiiBaseTest.php b/tests/unit/framework/YiiBaseTest.php index df12bf9..47474f2 100644 --- a/tests/unit/framework/YiiBaseTest.php +++ b/tests/unit/framework/YiiBaseTest.php @@ -1,6 +1,7 @@ aliases = Yii::$aliases; + } + + public function tearDown() + { + Yii::$aliases = $this->aliases; + } + public function testAlias() { + $this->assertEquals(YII_PATH, Yii::getAlias('@yii')); + + Yii::$aliases = array(); + $this->assertFalse(Yii::getAlias('@yii', false)); + + Yii::setAlias('@yii', '/yii/framework'); + $this->assertEquals('/yii/framework', Yii::getAlias('@yii')); + $this->assertEquals('/yii/framework/test/file', Yii::getAlias('@yii/test/file')); + Yii::setAlias('@yii/gii', '/yii/gii'); + $this->assertEquals('/yii/framework', Yii::getAlias('@yii')); + $this->assertEquals('/yii/framework/test/file', Yii::getAlias('@yii/test/file')); + $this->assertEquals('/yii/gii', Yii::getAlias('@yii/gii')); + $this->assertEquals('/yii/gii/file', Yii::getAlias('@yii/gii/file')); + + Yii::setAlias('@tii', '@yii/test'); + $this->assertEquals('/yii/framework/test', Yii::getAlias('@tii')); + Yii::setAlias('@yii', null); + $this->assertFalse(Yii::getAlias('@yii', false)); + $this->assertEquals('/yii/gii/file', Yii::getAlias('@yii/gii/file')); } public function testGetVersion() { - echo \Yii::getVersion(); + echo Yii::getVersion(); $this->assertTrue((boolean)preg_match('~\d+\.\d+(?:\.\d+)?(?:-\w+)?~', \Yii::getVersion())); } public function testPowered() { - $this->assertTrue(is_string(\Yii::powered())); + $this->assertTrue(is_string(Yii::powered())); } } diff --git a/tests/unit/framework/base/ModelTest.php b/tests/unit/framework/base/ModelTest.php index aa15230..f04e550 100644 --- a/tests/unit/framework/base/ModelTest.php +++ b/tests/unit/framework/base/ModelTest.php @@ -195,7 +195,7 @@ class ModelTest extends TestCase public function testCreateValidators() { - $this->setExpectedException('yii\base\InvalidConfigException', 'Invalid validation rule: a rule must be an array specifying both attribute names and validator type.'); + $this->setExpectedException('yii\base\InvalidConfigException', 'Invalid validation rule: a rule must specify both attribute names and validator type.'); $invalid = new InvalidRulesModel(); $invalid->createValidators(); diff --git a/tests/unit/framework/caching/CacheTest.php b/tests/unit/framework/caching/CacheTest.php index ad2fcf5..f9db4f4 100644 --- a/tests/unit/framework/caching/CacheTest.php +++ b/tests/unit/framework/caching/CacheTest.php @@ -16,9 +16,9 @@ abstract class CacheTest extends TestCase public function testSet() { $cache = $this->getCacheInstance(); - $cache->set('string_test', 'string_test'); - $cache->set('number_test', 42); - $cache->set('array_test', array('array_test' => 'array_test')); + $this->assertTrue($cache->set('string_test', 'string_test')); + $this->assertTrue($cache->set('number_test', 42)); + $this->assertTrue($cache->set('array_test', array('array_test' => 'array_test'))); $cache['arrayaccess_test'] = new \stdClass(); } @@ -45,7 +45,7 @@ abstract class CacheTest extends TestCase public function testExpire() { $cache = $this->getCacheInstance(); - $cache->set('expire_test', 'expire_test', 2); + $this->assertTrue($cache->set('expire_test', 'expire_test', 2)); sleep(1); $this->assertEquals('expire_test', $cache->get('expire_test')); sleep(2); @@ -57,11 +57,11 @@ abstract class CacheTest extends TestCase $cache = $this->getCacheInstance(); // should not change existing keys - $cache->add('number_test', 13); + $this->assertFalse($cache->add('number_test', 13)); $this->assertEquals(42, $cache->get('number_test')); // should store data is it's not there yet - $cache->add('add_test', 13); + $this->assertTrue($cache->add('add_test', 13)); $this->assertEquals(13, $cache->get('add_test')); } @@ -69,14 +69,14 @@ abstract class CacheTest extends TestCase { $cache = $this->getCacheInstance(); - $cache->delete('number_test'); + $this->assertTrue($cache->delete('number_test')); $this->assertEquals(null, $cache->get('number_test')); } public function testFlush() { $cache = $this->getCacheInstance(); - $cache->flush(); + $this->assertTrue($cache->flush()); $this->assertEquals(null, $cache->get('add_test')); } } diff --git a/tests/unit/framework/helpers/HtmlTest.php b/tests/unit/framework/helpers/HtmlTest.php index bf0ca0a..4077043 100644 --- a/tests/unit/framework/helpers/HtmlTest.php +++ b/tests/unit/framework/helpers/HtmlTest.php @@ -22,6 +22,14 @@ class HtmlTest extends \yii\test\TestCase )); } + public function assertEqualsWithoutLE($expected, $actual) + { + $expected = str_replace("\r\n", "\n", $expected); + $actual = str_replace("\r\n", "\n", $actual); + + $this->assertEquals($expected, $actual); + } + public function tearDown() { Yii::$app = null; @@ -240,21 +248,21 @@ class HtmlTest extends \yii\test\TestCase EOD; - $this->assertEquals($expected, Html::dropDownList('test')); + $this->assertEqualsWithoutLE($expected, Html::dropDownList('test')); $expected = << EOD; - $this->assertEquals($expected, Html::dropDownList('test', null, $this->getDataItems())); + $this->assertEqualsWithoutLE($expected, Html::dropDownList('test', null, $this->getDataItems())); $expected = << EOD; - $this->assertEquals($expected, Html::dropDownList('test', 'value2', $this->getDataItems())); + $this->assertEqualsWithoutLE($expected, Html::dropDownList('test', 'value2', $this->getDataItems())); } public function testListBox() @@ -264,48 +272,48 @@ EOD; EOD; - $this->assertEquals($expected, Html::listBox('test')); + $this->assertEqualsWithoutLE($expected, Html::listBox('test')); $expected = << EOD; - $this->assertEquals($expected, Html::listBox('test', null, $this->getDataItems(), array('size' => 5))); + $this->assertEqualsWithoutLE($expected, Html::listBox('test', null, $this->getDataItems(), array('size' => 5))); $expected = << EOD; - $this->assertEquals($expected, Html::listBox('test', null, $this->getDataItems2())); + $this->assertEqualsWithoutLE($expected, Html::listBox('test', null, $this->getDataItems2())); $expected = << EOD; - $this->assertEquals($expected, Html::listBox('test', 'value2', $this->getDataItems())); + $this->assertEqualsWithoutLE($expected, Html::listBox('test', 'value2', $this->getDataItems())); $expected = << EOD; - $this->assertEquals($expected, Html::listBox('test', array('value1', 'value2'), $this->getDataItems())); + $this->assertEqualsWithoutLE($expected, Html::listBox('test', array('value1', 'value2'), $this->getDataItems())); $expected = << EOD; - $this->assertEquals($expected, Html::listBox('test', null, array(), array('multiple' => true))); + $this->assertEqualsWithoutLE($expected, Html::listBox('test', null, array(), array('multiple' => true))); $expected = << EOD; - $this->assertEquals($expected, Html::listBox('test', '', array(), array('unselect' => '0'))); + $this->assertEqualsWithoutLE($expected, Html::listBox('test', '', array(), array('unselect' => '0'))); } public function testCheckboxList() @@ -316,19 +324,19 @@ EOD; EOD; - $this->assertEquals($expected, Html::checkboxList('test', array('value2'), $this->getDataItems())); + $this->assertEqualsWithoutLE($expected, Html::checkboxList('test', array('value2'), $this->getDataItems())); $expected = << text1<> EOD; - $this->assertEquals($expected, Html::checkboxList('test', array('value2'), $this->getDataItems2())); + $this->assertEqualsWithoutLE($expected, Html::checkboxList('test', array('value2'), $this->getDataItems2())); $expected = <<
    EOD; - $this->assertEquals($expected, Html::checkboxList('test', array('value2'), $this->getDataItems(), array( + $this->assertEqualsWithoutLE($expected, Html::checkboxList('test', array('value2'), $this->getDataItems(), array( 'separator' => "
    \n", 'unselect' => '0', ))); @@ -337,7 +345,7 @@ EOD; 0 1 EOD; - $this->assertEquals($expected, Html::checkboxList('test', array('value2'), $this->getDataItems(), array( + $this->assertEqualsWithoutLE($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)); } @@ -352,19 +360,19 @@ EOD; EOD; - $this->assertEquals($expected, Html::radioList('test', array('value2'), $this->getDataItems())); + $this->assertEqualsWithoutLE($expected, Html::radioList('test', array('value2'), $this->getDataItems())); $expected = << text1<> EOD; - $this->assertEquals($expected, Html::radioList('test', array('value2'), $this->getDataItems2())); + $this->assertEqualsWithoutLE($expected, Html::radioList('test', array('value2'), $this->getDataItems2())); $expected = <<
    EOD; - $this->assertEquals($expected, Html::radioList('test', array('value2'), $this->getDataItems(), array( + $this->assertEqualsWithoutLE($expected, Html::radioList('test', array('value2'), $this->getDataItems(), array( 'separator' => "
    \n", 'unselect' => '0', ))); @@ -373,7 +381,7 @@ EOD; 0 1 EOD; - $this->assertEquals($expected, Html::radioList('test', array('value2'), $this->getDataItems(), array( + $this->assertEqualsWithoutLE($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)); } @@ -420,7 +428,7 @@ EOD; 'group12' => array('class' => 'group'), ), ); - $this->assertEquals($expected, Html::renderSelectOptions(array('value111', 'value1'), $data, $attributes)); + $this->assertEqualsWithoutLE($expected, Html::renderSelectOptions(array('value111', 'value1'), $data, $attributes)); } public function testRenderAttributes()