From 54c539dd5183aac90910973413f0d8f753d49fda Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Thu, 7 Feb 2013 15:46:15 -0500 Subject: [PATCH 01/84] Fixed buildInCondition bug. --- framework/db/QueryBuilder.php | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/framework/db/QueryBuilder.php b/framework/db/QueryBuilder.php index ebca888..67dcbb0 100644 --- a/framework/db/QueryBuilder.php +++ b/framework/db/QueryBuilder.php @@ -592,21 +592,19 @@ class QueryBuilder extends \yii\base\Object return $operator === 'IN' ? '0=1' : ''; } - if (is_array($column)) { - if (count($column) > 1) { - return $this->buildCompositeInCondition($operator, $column, $values); + if (count($column) > 1) { + return $this->buildCompositeInCondition($operator, $column, $values); + } elseif (is_array($column)) { + $column = reset($column); + } + foreach ($values as $i => $value) { + if (is_array($value)) { + $value = isset($value[$column]) ? $value[$column] : null; + } + if ($value === null) { + $values[$i] = 'NULL'; } else { - $column = reset($column); - foreach ($values as $i => $value) { - if (is_array($value)) { - $value = isset($value[$column]) ? $value[$column] : null; - } - if ($value === null) { - $values[$i] = 'NULL'; - } else { - $values[$i] = is_string($value) ? $this->db->quoteValue($value) : (string)$value; - } - } + $values[$i] = is_string($value) ? $this->db->quoteValue($value) : (string)$value; } } if (strpos($column, '(') === false) { From 80e29130dc71665768fd67bf5cec148a8dc3482c Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Fri, 8 Feb 2013 01:37:07 +0400 Subject: [PATCH 02/84] more todos --- todo.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/todo.md b/todo.md index 7249235..6282ec5 100644 --- a/todo.md +++ b/todo.md @@ -6,14 +6,15 @@ * key-value-based (should allow storage-specific methods additionally to generic ones) * redis (put it under framework/db/redis or perhaps framework/caching?) - base - * TwigViewRenderer - * SmartyViewRenderer + * TwigViewRenderer (Alex) + * SmartyViewRenderer (Alex) - logging * WebTarget (TBD after web is in place): should consider using javascript and make it into a toolbar * ProfileTarget (TBD after web is in place): should consider using javascript and make it into a toolbar * unit tests - caching * backend-specific unit tests + * dependency unit tests - validators * FileValidator: depends on CUploadedFile * CaptchaValidator: depends on CaptchaAction @@ -27,6 +28,7 @@ - Module should be able to define its own configuration including routes. Application should be able to overwrite it. * application * security + - backport 1.1 changes - built-in console commands + api doc builder * support for markdown syntax From 45020ebc72370b5611f35b5800578ccd1f6e8825 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Fri, 8 Feb 2013 03:23:48 +0400 Subject: [PATCH 03/84] autoloader draft docs --- docs/autoloader.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 docs/autoloader.md diff --git a/docs/autoloader.md b/docs/autoloader.md new file mode 100644 index 0000000..b7696d7 --- /dev/null +++ b/docs/autoloader.md @@ -0,0 +1,19 @@ +Yii2 class loader +================= + +Yii 2 class loader is PSR-0 compliant. That means it can handle most of the PHP +libraries and frameworks out there. + +In order to autoload a library you need to set a root alias for it. + +PEAR-style libraries +-------------------- + +```php +\Yii::setAlias('@Twig', '@app/vendors/Twig'); +``` + +References +---------- + +- YiiBase::autoload \ No newline at end of file From 379fddfec35f3045a55211264d0b73afc6b9a675 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Fri, 8 Feb 2013 08:41:22 -0500 Subject: [PATCH 04/84] URL manager WIP --- framework/web/UrlManager.php | 5 +++ framework/web/UrlRule.php | 95 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 100 insertions(+) create mode 100644 framework/web/UrlRule.php diff --git a/framework/web/UrlManager.php b/framework/web/UrlManager.php index e890740..1c80938 100644 --- a/framework/web/UrlManager.php +++ b/framework/web/UrlManager.php @@ -49,4 +49,9 @@ class UrlManager extends Component else return ''; } + + public function createUrl($route, $params = array(), $ampersand = '&') + { + + } } diff --git a/framework/web/UrlRule.php b/framework/web/UrlRule.php new file mode 100644 index 0000000..096c9ba --- /dev/null +++ b/framework/web/UrlRule.php @@ -0,0 +1,95 @@ + + * @since 2.0 + */ +class UrlRule extends Object +{ + /** + * @var string the URL suffix used for this rule. + * For example, ".html" can be used so that the URL looks like pointing to a static HTML page. + * Defaults to null, meaning using the value of {@link CUrlManager::urlSuffix}. + */ + public $urlSuffix; + /** + * @var boolean whether the rule is case sensitive. Defaults to null, meaning + * using the value of {@link CUrlManager::caseSensitive}. + */ + public $caseSensitive; + /** + * @var array the default GET parameters (name=>value) that this rule provides. + * When this rule is used to parse the incoming request, the values declared in this property + * will be injected into $_GET. + */ + public $defaultParams = array(); + /** + * @var boolean whether the GET parameter values should match the corresponding + * sub-patterns in the rule when creating a URL. Defaults to null, meaning using the value + * of {@link CUrlManager::matchValue}. When this property is false, it means + * a rule will be used for creating a URL if its route and parameter names match the given ones. + * If this property is set true, then the given parameter values must also match the corresponding + * parameter sub-patterns. Note that setting this property to true will degrade performance. + * @since 1.1.0 + */ + public $matchValue; + /** + * @var string the HTTP verb (e.g. GET, POST, DELETE) that this rule should match. + * If this rule can match multiple verbs, please separate them with commas. + * If this property is not set, the rule can match any verb. + * Note that this property is only used when parsing a request. It is ignored for URL creation. + * @since 1.1.7 + */ + public $verb; + /** + * @var boolean whether this rule is only used for request parsing. + * Defaults to false, meaning the rule is used for both URL parsing and creation. + * @since 1.1.7 + */ + public $parsingOnly = false; + /** + * @var string the controller/action pair + */ + public $route; + /** + * @var array the mapping from route param name to token name (e.g. _r1=><1>) + */ + public $references = array(); + /** + * @var string the pattern used to match route + */ + public $routePattern; + /** + * @var string regular expression used to parse a URL + */ + public $pattern; + /** + * @var string template used to construct a URL + */ + public $template; + /** + * @var array list of parameters (name=>regular expression) + */ + public $params = array(); + /** + * @var boolean whether the URL allows additional parameters at the end of the path info. + */ + public $append; + /** + * @var boolean whether host info should be considered for this rule + */ + public $hasHostInfo; +} From ab42ab2d52c96661d802a0e01b43b6988e0da10a Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Fri, 8 Feb 2013 16:32:43 -0500 Subject: [PATCH 05/84] url wip --- framework/web/UrlManager.php | 68 +++++++++++++++++++++++++++++++++-- framework/web/UrlRule.php | 84 +++++++++++--------------------------------- 2 files changed, 87 insertions(+), 65 deletions(-) diff --git a/framework/web/UrlManager.php b/framework/web/UrlManager.php index 1c80938..4b9a606 100644 --- a/framework/web/UrlManager.php +++ b/framework/web/UrlManager.php @@ -19,7 +19,70 @@ use \yii\base\Component; */ class UrlManager extends Component { + /** + * @var array the URL rules (pattern=>route). + */ + public $rules = array(); + /** + * @var string the URL suffix used when in 'path' format. + * For example, ".html" can be used so that the URL looks like pointing to a static HTML page. Defaults to empty. + */ + public $urlSuffix = ''; + /** + * @var boolean whether to show entry script name in the constructed URL. Defaults to true. + */ + public $showScriptName = true; + /** + * @var boolean whether to append GET parameters to the path info part. Defaults to true. + * This property is only effective when {@link urlFormat} is 'path' and is mainly used when + * creating URLs. When it is true, GET parameters will be appended to the path info and + * separate from each other using slashes. If this is false, GET parameters will be in query part. + */ + public $appendParams = true; + /** + * @var string the GET variable name for route. Defaults to 'r'. + */ public $routeVar = 'r'; + /** + * @var boolean whether routes are case-sensitive. Defaults to true. By setting this to false, + * the route in the incoming request will be turned to lower case first before further processing. + * As a result, you should follow the convention that you use lower case when specifying + * controller mapping ({@link CWebApplication::controllerMap}) and action mapping + * ({@link CController::actions}). Also, the directory names for organizing controllers should + * be in lower case. + */ + public $caseSensitive = true; + /** + * @var boolean whether the GET parameter values should match the corresponding + * sub-patterns in a rule before using it to create a URL. Defaults to false, meaning + * a rule will be used for creating a URL only if its route and parameter names match the given ones. + * If this property is set true, then the given parameter values must also match the corresponding + * parameter sub-patterns. Note that setting this property to true will degrade performance. + * @since 1.1.0 + */ + public $matchValue = false; + /** + * @var string the ID of the cache application component that is used to cache the parsed URL rules. + * Defaults to 'cache' which refers to the primary cache application component. + * Set this property to false if you want to disable caching URL rules. + */ + public $cacheID = 'cache'; + /** + * @var boolean whether to enable strict URL parsing. + * This property is only effective when {@link urlFormat} is 'path'. + * If it is set true, then an incoming URL must match one of the {@link rules URL rules}. + * Otherwise, it will be treated as an invalid request and trigger a 404 HTTP exception. + * Defaults to false. + */ + public $useStrictParsing = false; + /** + * @var string the class name or path alias for the URL rule instances. Defaults to 'CUrlRule'. + * If you change this to something else, please make sure that the new class must extend from + * {@link CBaseUrlRule} and have the same constructor signature as {@link CUrlRule}. + * It must also be serializable and autoloadable. + * @since 1.1.8 + */ + public $urlRuleClass = 'CUrlRule'; /** * Initializes the application component. @@ -44,10 +107,11 @@ class UrlManager extends Component */ public function parseUrl($request) { - if(isset($_GET[$this->routeVar])) + if (isset($_GET[$this->routeVar])) { return $_GET[$this->routeVar]; - else + } else { return ''; + } } public function createUrl($route, $params = array(), $ampersand = '&') diff --git a/framework/web/UrlRule.php b/framework/web/UrlRule.php index 096c9ba..1a4e77e 100644 --- a/framework/web/UrlRule.php +++ b/framework/web/UrlRule.php @@ -13,6 +13,11 @@ use yii\base\Object; /** * UrlManager manages the URLs of Yii applications. + * array( + * 'pattern' => 'post/', + * 'route' => 'post/view', + * 'params' => array('id' => 1), + * ) * * @author Qiang Xue * @since 2.0 @@ -20,76 +25,29 @@ use yii\base\Object; class UrlRule extends Object { /** - * @var string the URL suffix used for this rule. - * For example, ".html" can be used so that the URL looks like pointing to a static HTML page. - * Defaults to null, meaning using the value of {@link CUrlManager::urlSuffix}. - */ - public $urlSuffix; - /** - * @var boolean whether the rule is case sensitive. Defaults to null, meaning - * using the value of {@link CUrlManager::caseSensitive}. + * @var string regular expression used to parse a URL */ - public $caseSensitive; + public $pattern; /** * @var array the default GET parameters (name=>value) that this rule provides. * When this rule is used to parse the incoming request, the values declared in this property * will be injected into $_GET. */ - public $defaultParams = array(); - /** - * @var boolean whether the GET parameter values should match the corresponding - * sub-patterns in the rule when creating a URL. Defaults to null, meaning using the value - * of {@link CUrlManager::matchValue}. When this property is false, it means - * a rule will be used for creating a URL if its route and parameter names match the given ones. - * If this property is set true, then the given parameter values must also match the corresponding - * parameter sub-patterns. Note that setting this property to true will degrade performance. - * @since 1.1.0 - */ - public $matchValue; - /** - * @var string the HTTP verb (e.g. GET, POST, DELETE) that this rule should match. - * If this rule can match multiple verbs, please separate them with commas. - * If this property is not set, the rule can match any verb. - * Note that this property is only used when parsing a request. It is ignored for URL creation. - * @since 1.1.7 - */ - public $verb; - /** - * @var boolean whether this rule is only used for request parsing. - * Defaults to false, meaning the rule is used for both URL parsing and creation. - * @since 1.1.7 - */ - public $parsingOnly = false; - /** - * @var string the controller/action pair - */ - public $route; - /** - * @var array the mapping from route param name to token name (e.g. _r1=><1>) - */ - public $references = array(); - /** - * @var string the pattern used to match route - */ - public $routePattern; - /** - * @var string regular expression used to parse a URL - */ - public $pattern; - /** - * @var string template used to construct a URL - */ - public $template; - /** - * @var array list of parameters (name=>regular expression) - */ public $params = array(); /** - * @var boolean whether the URL allows additional parameters at the end of the path info. - */ - public $append; - /** - * @var boolean whether host info should be considered for this rule + * @var string the route to the controller action */ - public $hasHostInfo; + public $route; + + public function createUrl($route, $params, $ampersand) + { + + } + + public function parse($path) + { + $route = ''; + $params = array(); + return array($route, $params); + } } From 712f4dae0d4a8ba87b8a8f91d02281703d4f038f Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Sun, 10 Feb 2013 14:53:35 -0500 Subject: [PATCH 06/84] url wip --- docs/api/db/ActiveRecord.md | 4 +- framework/web/UrlManager.php | 38 ++- framework/web/UrlRule.php | 130 +++++++- tests/unit/framework/web/UrlManagerTest.php | 7 + tests/unit/framework/web/UrlRuleTest.php | 441 ++++++++++++++++++++++++++++ 5 files changed, 603 insertions(+), 17 deletions(-) create mode 100644 tests/unit/framework/web/UrlManagerTest.php create mode 100644 tests/unit/framework/web/UrlRuleTest.php diff --git a/docs/api/db/ActiveRecord.md b/docs/api/db/ActiveRecord.md index da281d8..822c548 100644 --- a/docs/api/db/ActiveRecord.md +++ b/docs/api/db/ActiveRecord.md @@ -300,7 +300,7 @@ foreach ($customers as $customer) { ~~~ How many SQL queries will be performed in the above code, assuming there are more than 100 customers in -the database? 101! The first SQL query brings back 100 customers. Then for each customer, another SQL query +the database? 101! The first SQL query brings back 100 customers. Then for each customer, a SQL query is performed to bring back the customer's orders. To solve the above performance problem, you can use the so-called *eager loading* by calling [[ActiveQuery::with()]]: @@ -318,7 +318,7 @@ foreach ($customers as $customer) { } ~~~ -As you can see, only two SQL queries were needed for the same task. +As you can see, only two SQL queries are needed for the same task. Sometimes, you may want to customize the relational queries on the fly. It can be diff --git a/framework/web/UrlManager.php b/framework/web/UrlManager.php index 4b9a606..e03f086 100644 --- a/framework/web/UrlManager.php +++ b/framework/web/UrlManager.php @@ -98,24 +98,50 @@ class UrlManager extends Component */ protected function processRules() { + foreach ($this->rules as $i => $rule) { + if (!isset($rule['class'])) { + $rule['class'] = 'yii\web\UrlRule'; + } + $this->rules[$i] = \Yii::createObject($rule); + } } /** * Parses the user request. - * @param HttpRequest $request the request application component + * @param Request $request the request application component * @return string the route (controllerID/actionID) and perhaps GET parameters in path format. */ public function parseUrl($request) { - if (isset($_GET[$this->routeVar])) { - return $_GET[$this->routeVar]; - } else { - return ''; + } + + public function createUrl($route, $params = array()) + { + $anchor = isset($params['#']) ? '#' . $params['#'] : ''; + unset($anchor['#']); + + /** @var $rule UrlRule */ + foreach ($this->rules as $rule) { + if (($url = $rule->createUrl($route, $params)) !== false) { + return $this->getBaseUrl() . $url . $anchor; + } } + + if ($params !== array()) { + $route .= '?' . http_build_query($params); + } + return $this->getBaseUrl() . '/' . $route . $anchor; } - public function createUrl($route, $params = array(), $ampersand = '&') + private $_baseUrl; + + public function getBaseUrl() { + return $this->_baseUrl; + } + public function setBaseUrl($value) + { + $this->_baseUrl = trim($value, '/'); } } diff --git a/framework/web/UrlRule.php b/framework/web/UrlRule.php index 1a4e77e..2961275 100644 --- a/framework/web/UrlRule.php +++ b/framework/web/UrlRule.php @@ -14,9 +14,15 @@ use yii\base\Object; /** * UrlManager manages the URLs of Yii applications. * array( - * 'pattern' => 'post/', + * 'pattern' => 'post/', * 'route' => 'post/view', - * 'params' => array('id' => 1), + * 'defaults' => array('page' => 1), + * ) + * + * array( + * 'pattern' => 'about', + * 'route' => 'site/page', + * 'defaults' => array('view' => 'about'), * ) * * @author Qiang Xue @@ -29,22 +35,128 @@ class UrlRule extends Object */ public $pattern; /** + * @var string the route to the controller action + */ + public $route; + /** * @var array the default GET parameters (name=>value) that this rule provides. * When this rule is used to parse the incoming request, the values declared in this property * will be injected into $_GET. */ - public $params = array(); - /** - * @var string the route to the controller action - */ - public $route; + public $defaults = array(); + + protected $paramRules = array(); + protected $routeRule; + protected $template; + protected $routeParams = array(); + + public function init() + { + $this->pattern = trim($this->pattern, '/'); + if ($this->pattern === '') { + $this->template = ''; + $this->pattern = '#^$#u'; + return; + } else { + $this->pattern = '/' . $this->pattern . '/'; + } + + $this->route = trim($this->route, '/'); + if (strpos($this->route, '<') !== false && preg_match_all('/<(\w+)>/', $this->route, $matches)) { + foreach ($matches[1] as $name) { + $this->routeParams[$name] = "<$name>"; + } + } + + $tr = $tr2 = array(); + if (preg_match_all('/<(\w+):?([^>]+)?>/', $this->pattern, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER)) { + foreach ($matches as $match) { + $name = $match[1][0]; + $pattern = isset($match[2][0]) ? $match[2][0] : '[^\/]+'; + if (isset($this->defaults[$name])) { + $length = strlen($match[0][0]); + $offset = $match[0][1]; + if ($this->pattern[$offset - 1] === '/' && $this->pattern[$offset + $length] === '/') { + $tr["<$name>"] = "(?P<$name>(?:/$pattern)?)"; + } else { + $tr["<$name>"] = "(?P<$name>(?:$pattern)?)"; + } + } else { + $tr["<$name>"] = "(?P<$name>$pattern)"; + } + if (isset($this->routeParams[$name])) { + $tr2["<$name>"] = "(?P<$name>$pattern)"; + } else { + $this->paramRules[$name] = $pattern === '[^\/]+' ? '' : "#^$pattern$#"; + } + } + } + + $this->template = preg_replace('/<(\w+):?([^>]+)?>/', '<$1>', $this->pattern); + $this->pattern = '#^' . strtr($this->template, $tr) . '$#u'; - public function createUrl($route, $params, $ampersand) + if ($this->routeParams !== array()) { + $this->routeRule = '#^' . strtr($this->route, $tr2) . '$#u'; + } + } + + public function parseUrl($pathInfo) { } - public function parse($path) + public function createUrl($route, $params) + { + $tr = array(); + + // match the route part first + if ($route !== $this->route) { + if ($this->routeRule !== null && preg_match($this->routeRule, $route, $matches)) { + foreach ($this->routeParams as $key => $name) { + $tr[$name] = $matches[$key]; + } + } else { + return false; + } + } + + // match default params + // if a default param is not in the route pattern, its value must also be matched + foreach ($this->defaults as $name => $value) { + if (!isset($params[$name])) { + return false; + } elseif (strcmp($params[$name], $value) === 0) { // strcmp will do string conversion automatically + unset($params[$name]); + if (isset($this->paramRules[$name])) { + $tr["<$name>"] = ''; + $tr["/<$name>/"] = '/'; + } + } elseif (!isset($this->paramRules[$name])) { + return false; + } + } + + // match params in the pattern + foreach ($this->paramRules as $name => $rule) { + if (isset($params[$name]) && ($rule === '' || preg_match($rule, $params[$name]))) { + $tr["<$name>"] = urlencode($params[$name]); + unset($params[$name]); + } elseif (!isset($this->defaults[$name]) || isset($params[$name])) { + return false; + } + } + + $url = trim(strtr($this->template, $tr), '/'); + if (strpos($url, '//') !== false) { + $url = preg_replace('#/+#', '/', $url); + } + if ($params !== array()) { + $url .= '?' . http_build_query($params); + } + return $url; + } + + public function parse($pathInfo) { $route = ''; $params = array(); diff --git a/tests/unit/framework/web/UrlManagerTest.php b/tests/unit/framework/web/UrlManagerTest.php new file mode 100644 index 0000000..3bd3007 --- /dev/null +++ b/tests/unit/framework/web/UrlManagerTest.php @@ -0,0 +1,7 @@ +getTestsForCreateUrl(); + foreach ($suites as $i => $suite) { + list ($name, $config, $tests) = $suite; + $rule = new UrlRule($config); + foreach ($tests as $j => $test) { + list ($route, $params, $expected) = $test; + $url = $rule->createUrl($route, $params); + $this->assertEquals($expected, $url, "Test#$i-$j: $name"); + } + } + } + + public function testParseUrl() + { + $suites = $this->getTestsForParseUrl(); + foreach ($suites as $i => $suite) { + list ($name, $config, $tests) = $suite; + $rule = new UrlRule($config); + foreach ($tests as $j => $test) { + $pathInfo = $test[0]; + $route = $test[1]; + $params = isset($test[2]) ? $test[2] : array(); + $result = $rule->parseUrl($pathInfo); + if ($route === false) { + $this->assertFalse($result, "Test#$i-$j: $name"); + } else { + $this->assertEquals(array($route, $params), $result, "Test#$i-$j: $name"); + } + } + } + } + + protected function getTestsForCreateUrl() + { + // structure of each test + // message for the test + // config for the URL rule + // list of inputs and outputs + // route + // params + // expected output + return array( + array( + 'empty pattern', + array( + 'pattern' => '', + 'route' => 'post/index', + ), + array( + array('post/index', array(), ''), + array('comment/index', array(), false), + array('post/index', array('page' => 1), '?page=1'), + ), + ), + array( + 'without param', + array( + 'pattern' => 'posts', + 'route' => 'post/index', + ), + array( + array('post/index', array(), 'posts'), + array('comment/index', array(), false), + array('post/index', array('page' => 1), 'posts?page=1'), + ), + ), + array( + 'with param', + array( + 'pattern' => 'post/', + 'route' => 'post/index', + ), + array( + array('post/index', array(), false), + array('comment/index', array(), false), + array('post/index', array('page' => 1), 'post/1'), + array('post/index', array('page' => 1, 'tag' => 'a'), 'post/1?tag=a'), + ), + ), + array( + 'with param requirement', + array( + 'pattern' => 'post/', + 'route' => 'post/index', + ), + array( + array('post/index', array('page' => 'abc'), false), + array('post/index', array('page' => 1), 'post/1'), + array('post/index', array('page' => 1, 'tag' => 'a'), 'post/1?tag=a'), + ), + ), + array( + 'with multiple params', + array( + 'pattern' => 'post/-', + 'route' => 'post/index', + ), + array( + array('post/index', array('page' => '1abc'), false), + array('post/index', array('page' => 1), false), + array('post/index', array('page' => 1, 'tag' => 'a'), 'post/1-a'), + ), + ), + array( + 'with optional param', + array( + 'pattern' => 'post//', + 'route' => 'post/index', + 'defaults' => array('page' => 1), + ), + array( + array('post/index', array('page' => 1), false), + array('post/index', array('page' => '1abc', 'tag' => 'a'), false), + array('post/index', array('page' => 1, 'tag' => 'a'), 'post/a'), + array('post/index', array('page' => 2, 'tag' => 'a'), 'post/2/a'), + ), + ), + array( + 'with optional param not in pattern', + array( + 'pattern' => 'post/', + 'route' => 'post/index', + 'defaults' => array('page' => 1), + ), + array( + array('post/index', array('page' => 1), false), + array('post/index', array('page' => '1abc', 'tag' => 'a'), false), + array('post/index', array('page' => 2, 'tag' => 'a'), false), + array('post/index', array('page' => 1, 'tag' => 'a'), 'post/a'), + ), + ), + array( + 'multiple optional params', + array( + 'pattern' => 'post///', + 'route' => 'post/index', + 'defaults' => array('page' => 1, 'sort' => 'yes'), + ), + array( + array('post/index', array('page' => 1), false), + array('post/index', array('page' => '1abc', 'tag' => 'a'), false), + array('post/index', array('page' => 1, 'tag' => 'a', 'sort' => 'YES'), false), + array('post/index', array('page' => 1, 'tag' => 'a', 'sort' => 'yes'), 'post/a'), + array('post/index', array('page' => 2, 'tag' => 'a', 'sort' => 'yes'), 'post/2/a'), + array('post/index', array('page' => 2, 'tag' => 'a', 'sort' => 'no'), 'post/2/a/no'), + array('post/index', array('page' => 1, 'tag' => 'a', 'sort' => 'no'), 'post/a/no'), + ), + ), + array( + 'optional param and required param separated by dashes', + array( + 'pattern' => 'post/-', + 'route' => 'post/index', + 'defaults' => array('page' => 1), + ), + array( + array('post/index', array('page' => 1), false), + array('post/index', array('page' => '1abc', 'tag' => 'a'), false), + array('post/index', array('page' => 1, 'tag' => 'a'), 'post/-a'), + array('post/index', array('page' => 2, 'tag' => 'a'), 'post/2-a'), + ), + ), + array( + 'optional param at the end', + array( + 'pattern' => 'post//', + 'route' => 'post/index', + 'defaults' => array('page' => 1), + ), + array( + array('post/index', array('page' => 1), false), + array('post/index', array('page' => '1abc', 'tag' => 'a'), false), + array('post/index', array('page' => 1, 'tag' => 'a'), 'post/a'), + array('post/index', array('page' => 2, 'tag' => 'a'), 'post/a/2'), + ), + ), + array( + 'consecutive optional params', + array( + 'pattern' => 'post//', + 'route' => 'post/index', + 'defaults' => array('page' => 1, 'tag' => 'a'), + ), + array( + array('post/index', array('page' => 1), false), + array('post/index', array('page' => '1abc', 'tag' => 'a'), false), + array('post/index', array('page' => 1, 'tag' => 'a'), 'post'), + array('post/index', array('page' => 2, 'tag' => 'a'), 'post/2'), + array('post/index', array('page' => 1, 'tag' => 'b'), 'post/b'), + array('post/index', array('page' => 2, 'tag' => 'b'), 'post/2/b'), + ), + ), + array( + 'consecutive optional params separated by dash', + array( + 'pattern' => 'post/-', + 'route' => 'post/index', + 'defaults' => array('page' => 1, 'tag' => 'a'), + ), + array( + array('post/index', array('page' => 1), false), + array('post/index', array('page' => '1abc', 'tag' => 'a'), false), + array('post/index', array('page' => 1, 'tag' => 'a'), 'post/-'), + array('post/index', array('page' => 1, 'tag' => 'b'), 'post/-b'), + array('post/index', array('page' => 2, 'tag' => 'a'), 'post/2-'), + array('post/index', array('page' => 2, 'tag' => 'b'), 'post/2-b'), + ), + ), + array( + 'route has parameters', + array( + 'pattern' => '/', + 'route' => '/', + 'defaults' => array(), + ), + array( + array('post/index', array('page' => 1), 'post/index?page=1'), + array('module/post/index', array(), false), + ), + ), + array( + 'route has parameters with regex', + array( + 'pattern' => '/', + 'route' => '/', + 'defaults' => array(), + ), + array( + array('post/index', array('page' => 1), 'post/index?page=1'), + array('comment/index', array('page' => 1), 'comment/index?page=1'), + array('test/index', array('page' => 1), false), + array('post', array(), false), + array('module/post/index', array(), false), + array('post/index', array('controller' => 'comment'), 'post/index?controller=comment'), + ), + ), + /* this is not supported + array( + 'route has default parameter', + array( + 'pattern' => '/', + 'route' => '/', + 'defaults' => array('action' => 'index'), + ), + array( + array('post/view', array('page' => 1), 'post/view?page=1'), + array('comment/view', array('page' => 1), 'comment/view?page=1'), + array('test/view', array('page' => 1), false), + array('post/index', array('page' => 1), 'post?page=1'), + ), + ), + */ + ); + } + + protected function getTestsForParseUrl() + { + // structure of each test + // message for the test + // config for the URL rule + // list of inputs and outputs + // pathInfo + // expected route, or false if the rule doesn't apply + // expected params, or not set if empty + return array( + array( + 'empty pattern', + array( + 'pattern' => '', + 'route' => 'post/index', + ), + array( + array('', 'post/index'), + array('a', false), + ), + ), + array( + 'without param', + array( + 'pattern' => 'posts', + 'route' => 'post/index', + ), + array( + array('posts', 'post/index'), + array('a', false), + ), + ), + array( + 'with param', + array( + 'pattern' => 'post/', + 'route' => 'post/index', + ), + array( + array('post/1', 'post/index', array('page' => '1')), + array('post/a', 'post/index', array('page' => 'a')), + array('post', false), + array('posts', false), + ), + ), + array( + 'with param requirement', + array( + 'pattern' => 'post/', + 'route' => 'post/index', + ), + array( + array('post/1', 'post/index', array('page' => '1')), + array('post/a', false), + array('post/1/a', false), + ), + ), + array( + 'with multiple params', + array( + 'pattern' => 'post/-', + 'route' => 'post/index', + ), + array( + array('post/1-a', 'post/index', array('page' => '1', 'tag' => 'a')), + array('post/a', false), + array('post/1', false), + array('post/1/a', false), + ), + ), + array( + 'with optional param', + array( + 'pattern' => 'post//', + 'route' => 'post/index', + 'defaults' => array('page' => 1), + ), + array( + array('post/1/a', 'post/index', array('page' => '1', 'tag' => 'a')), + array('post/2/a', 'post/index', array('page' => '2', 'tag' => 'a')), + array('post/a', 'post/index', array('page' => '1', 'tag' => 'a')), + array('post/1', 'post/index', array('page' => '1', 'tag' => '1')), + ), + ), + array( + 'with optional param not in pattern', + array( + 'pattern' => 'post/', + 'route' => 'post/index', + 'defaults' => array('page' => 1), + ), + array( + array('post/a', 'post/index', array('page' => '1', 'tag' => 'a')), + array('post/1', 'post/index', array('page' => '1', 'tag' => '1')), + array('post', false), + ), + ), + array( + 'multiple optional params', + array( + 'pattern' => 'post///', + 'route' => 'post/index', + 'defaults' => array('page' => 1, 'sort' => 'yes'), + ), + array( + array('post/1/a/yes', 'post/index', array('page' => '1', 'tag' => 'a', 'sort' => 'yes')), + array('post/2/a/no', 'post/index', array('page' => '2', 'tag' => 'a', 'sort' => 'no')), + array('post/2/a', 'post/index', array('page' => '2', 'tag' => 'a', 'sort' => 'yes')), + array('post/a/no', 'post/index', array('page' => '1', 'tag' => 'a', 'sort' => 'no')), + array('post/a', 'post/index', array('page' => '1', 'tag' => 'a', 'sort' => 'yes')), + array('post', false), + ), + ), + array( + 'optional param and required param separated by dashes', + array( + 'pattern' => 'post/-', + 'route' => 'post/index', + 'defaults' => array('page' => 1), + ), + array( + array('post/1-a', 'post/index', array('page' => '1', 'tag' => 'a')), + array('post/2-a', 'post/index', array('page' => '2', 'tag' => 'a')), + array('post/-a', 'post/index', array('page' => '1', 'tag' => 'a')), + array('post/a', false), + array('post-a', false), + ), + ), + array( + 'optional param at the end', + array( + 'pattern' => 'post//', + 'route' => 'post/index', + 'defaults' => array('page' => 1), + ), + array( + array('post/a/1', 'post/index', array('page' => '1', 'tag' => 'a')), + array('post/a/2', 'post/index', array('page' => '2', 'tag' => 'a')), + array('post/a', 'post/index', array('page' => '1', 'tag' => 'a')), + array('post/2', 'post/index', array('page' => '1', 'tag' => '2')), + array('post', false), + ), + ), + array( + 'consecutive optional params', + array( + 'pattern' => 'post//', + 'route' => 'post/index', + 'defaults' => array('page' => 1, 'tag' => 'a'), + ), + array( + array('post/2/b', 'post/index', array('page' => '2', 'tag' => 'b')), + array('post/2', 'post/index', array('page' => '2', 'tag' => 'a')), + array('post', 'post/index', array('page' => '1', 'tag' => 'a')), + array('post/b', 'post/index', array('page' => '1', 'tag' => 'b')), + array('post//b', false), + ), + ), + array( + 'consecutive optional params separated by dash', + array( + 'pattern' => 'post/-', + 'route' => 'post/index', + 'defaults' => array('page' => 1, 'tag' => 'a'), + ), + array( + array('post/2-b', 'post/index', array('page' => '2', 'tag' => 'b')), + array('post/2-', 'post/index', array('page' => '2', 'tag' => 'a')), + array('post/-b', 'post/index', array('page' => '1', 'tag' => 'b')), + array('post/-', 'post/index', array('page' => '1', 'tag' => 'a')), + array('post', false), + ), + ), + ); + } +} From f31369e7c177d45eb94a82964ac8cbc1b08fc1f0 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Sun, 10 Feb 2013 21:55:15 -0500 Subject: [PATCH 07/84] url wip --- framework/web/UrlRule.php | 12 ++++++++++-- tests/unit/framework/web/UrlRuleTest.php | 3 +-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/framework/web/UrlRule.php b/framework/web/UrlRule.php index 2961275..457b735 100644 --- a/framework/web/UrlRule.php +++ b/framework/web/UrlRule.php @@ -112,8 +112,13 @@ class UrlRule extends Object // match the route part first if ($route !== $this->route) { if ($this->routeRule !== null && preg_match($this->routeRule, $route, $matches)) { - foreach ($this->routeParams as $key => $name) { - $tr[$name] = $matches[$key]; + foreach ($this->routeParams as $name => $token) { + if (isset($this->defaults[$name]) && strcmp($this->defaults[$name], $matches[$name]) === 0) { + $tr[$token] = ''; + $tr["/$token/"] = '/'; + } else { + $tr[$token] = $matches[$name]; + } } } else { return false; @@ -123,6 +128,9 @@ class UrlRule extends Object // match default params // if a default param is not in the route pattern, its value must also be matched foreach ($this->defaults as $name => $value) { + if (isset($this->routeParams[$name])) { + continue; + } if (!isset($params[$name])) { return false; } elseif (strcmp($params[$name], $value) === 0) { // strcmp will do string conversion automatically diff --git a/tests/unit/framework/web/UrlRuleTest.php b/tests/unit/framework/web/UrlRuleTest.php index a73e395..1467454 100644 --- a/tests/unit/framework/web/UrlRuleTest.php +++ b/tests/unit/framework/web/UrlRuleTest.php @@ -244,7 +244,6 @@ class UrlRuleTest extends \yiiunit\TestCase array('post/index', array('controller' => 'comment'), 'post/index?controller=comment'), ), ), - /* this is not supported array( 'route has default parameter', array( @@ -256,10 +255,10 @@ class UrlRuleTest extends \yiiunit\TestCase array('post/view', array('page' => 1), 'post/view?page=1'), array('comment/view', array('page' => 1), 'comment/view?page=1'), array('test/view', array('page' => 1), false), + array('test/index', array('page' => 1), false), array('post/index', array('page' => 1), 'post?page=1'), ), ), - */ ); } From a946a863861f42f7f13c2a334015152acb24bbf9 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Mon, 11 Feb 2013 08:49:44 -0500 Subject: [PATCH 08/84] fixed parseUrl. --- framework/web/UrlRule.php | 36 +++++++++++++++++++++++------------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/framework/web/UrlRule.php b/framework/web/UrlRule.php index 457b735..247633e 100644 --- a/framework/web/UrlRule.php +++ b/framework/web/UrlRule.php @@ -77,9 +77,9 @@ class UrlRule extends Object $length = strlen($match[0][0]); $offset = $match[0][1]; if ($this->pattern[$offset - 1] === '/' && $this->pattern[$offset + $length] === '/') { - $tr["<$name>"] = "(?P<$name>(?:/$pattern)?)"; + $tr["/<$name>"] = "(/(?P<$name>$pattern))?"; } else { - $tr["<$name>"] = "(?P<$name>(?:$pattern)?)"; + $tr["<$name>"] = "(?P<$name>$pattern)?"; } } else { $tr["<$name>"] = "(?P<$name>$pattern)"; @@ -93,7 +93,7 @@ class UrlRule extends Object } $this->template = preg_replace('/<(\w+):?([^>]+)?>/', '<$1>', $this->pattern); - $this->pattern = '#^' . strtr($this->template, $tr) . '$#u'; + $this->pattern = '#^' . trim(strtr($this->template, $tr), '/') . '$#u'; if ($this->routeParams !== array()) { $this->routeRule = '#^' . strtr($this->route, $tr2) . '$#u'; @@ -102,7 +102,26 @@ class UrlRule extends Object public function parseUrl($pathInfo) { - + if (!preg_match($this->pattern, $pathInfo, $matches)) { + return false; + } + $params = $this->defaults; + $tr = array(); + foreach ($matches as $name => $value) { + if ($value !== '') { + if (isset($this->routeParams[$name])) { + $tr[$this->routeParams[$name]] = $value; + } elseif (isset($this->paramRules[$name])) { + $params[$name] = $value; + } + } + } + if ($this->routeRule !== null) { + $route = strtr($this->route, $tr); + } else { + $route = $this->route; + } + return array($route, $params); } public function createUrl($route, $params) @@ -115,7 +134,6 @@ class UrlRule extends Object foreach ($this->routeParams as $name => $token) { if (isset($this->defaults[$name]) && strcmp($this->defaults[$name], $matches[$name]) === 0) { $tr[$token] = ''; - $tr["/$token/"] = '/'; } else { $tr[$token] = $matches[$name]; } @@ -137,7 +155,6 @@ class UrlRule extends Object unset($params[$name]); if (isset($this->paramRules[$name])) { $tr["<$name>"] = ''; - $tr["/<$name>/"] = '/'; } } elseif (!isset($this->paramRules[$name])) { return false; @@ -163,11 +180,4 @@ class UrlRule extends Object } return $url; } - - public function parse($pathInfo) - { - $route = ''; - $params = array(); - return array($route, $params); - } } From e25ad4bc88810e97f55585c9204a72eb7ea19470 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Mon, 11 Feb 2013 13:29:53 -0500 Subject: [PATCH 09/84] URL wip. --- framework/web/UrlRule.php | 58 ++++++++++++++++++++------------ tests/unit/framework/web/UrlRuleTest.php | 44 ++++++++++++++++++++++++ 2 files changed, 81 insertions(+), 21 deletions(-) diff --git a/framework/web/UrlRule.php b/framework/web/UrlRule.php index 247633e..612a793 100644 --- a/framework/web/UrlRule.php +++ b/framework/web/UrlRule.php @@ -1,6 +1,6 @@ 'post/', - * 'route' => 'post/view', - * 'defaults' => array('page' => 1), - * ) - * - * array( - * 'pattern' => 'about', - * 'route' => 'site/page', - * 'defaults' => array('view' => 'about'), - * ) + * UrlRule represents a rule used for parsing and generating URLs. * * @author Qiang Xue * @since 2.0 @@ -45,13 +34,36 @@ class UrlRule extends Object */ public $defaults = array(); - protected $paramRules = array(); - protected $routeRule; + /** + * @var string the template for generating a new URL. This is derived from [[pattern]] and is used in generating URL. + */ protected $template; + /** + * @var string the regex for matching the route part. This is used in generating URL. + */ + protected $routeRule; + /** + * @var array list of regex for matching parameters. This is used in generating URL. + */ + protected $paramRules = array(); + /** + * @var array list of parameters used in the route. + */ protected $routeParams = array(); + /** + * Initializes this rule. + */ public function init() { + $this->compileRule(); + } + + /** + * Compiles the rule using the current configuration. + */ + protected function compileRule() + { $this->pattern = trim($this->pattern, '/'); if ($this->pattern === '') { $this->template = ''; @@ -105,15 +117,19 @@ class UrlRule extends Object if (!preg_match($this->pattern, $pathInfo, $matches)) { return false; } + foreach ($this->defaults as $name => $value) { + if (!isset($matches[$name]) || $matches[$name] === '') { + $matches[$name] = $value; + } + } $params = $this->defaults; $tr = array(); foreach ($matches as $name => $value) { - if ($value !== '') { - if (isset($this->routeParams[$name])) { - $tr[$this->routeParams[$name]] = $value; - } elseif (isset($this->paramRules[$name])) { - $params[$name] = $value; - } + if (isset($this->routeParams[$name])) { + $tr[$this->routeParams[$name]] = $value; + unset($params[$name]); + } elseif (isset($this->paramRules[$name])) { + $params[$name] = $value; } } if ($this->routeRule !== null) { diff --git a/tests/unit/framework/web/UrlRuleTest.php b/tests/unit/framework/web/UrlRuleTest.php index 1467454..e9148c7 100644 --- a/tests/unit/framework/web/UrlRuleTest.php +++ b/tests/unit/framework/web/UrlRuleTest.php @@ -435,6 +435,50 @@ class UrlRuleTest extends \yiiunit\TestCase array('post', false), ), ), + array( + 'route has parameters', + array( + 'pattern' => '/', + 'route' => '/', + 'defaults' => array(), + ), + array( + array('post/index', 'post/index'), + array('module/post/index', false), + ), + ), + array( + 'route has parameters with regex', + array( + 'pattern' => '/', + 'route' => '/', + 'defaults' => array(), + ), + array( + array('post/index', 'post/index'), + array('comment/index', 'comment/index'), + array('test/index', false), + array('post', false), + array('module/post/index', false), + ), + ), + array( + 'route has default parameter', + array( + 'pattern' => '/', + 'route' => '/', + 'defaults' => array('action' => 'index'), + ), + array( + array('post/view', 'post/view'), + array('comment/view', 'comment/view'), + array('test/view', false), + array('post', 'post/index'), + array('posts', false), + array('test', false), + array('index', false), + ), + ), ); } } From 842caa3aaedfde0477ae21fc6f6bf5a7225479d6 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Tue, 12 Feb 2013 00:08:28 +0400 Subject: [PATCH 10/84] Yii-style for PHP fatal and parse errors --- framework/base/Application.php | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/framework/base/Application.php b/framework/base/Application.php index 55dc0cc..b0167df 100644 --- a/framework/base/Application.php +++ b/framework/base/Application.php @@ -126,6 +126,7 @@ class Application extends Module */ public function init() { + ob_start(); $this->preloadComponents(); } @@ -138,10 +139,27 @@ class Application extends Module */ public function end($status = 0, $exit = true) { + $lastError = error_get_last(); + + if(isset($lastError['type']) && in_array($lastError['type'], array(E_ERROR, E_PARSE))) { + ob_end_clean(); + $exception = new \ErrorException($lastError['message'], 0, $lastError['type'], $lastError['file'], $lastError['line']); + $this->logException($exception); + + if (($handler = $this->getErrorHandler()) !== null) { + $handler->handle($exception); + } else { + $this->renderException($exception); + } + + die(1); + } + if (!$this->_ended) { $this->_ended = true; $this->afterRequest(); } + ob_end_flush(); if ($exit) { exit($status); } @@ -155,6 +173,7 @@ class Application extends Module public function run() { $this->beforeRequest(); + register_shutdown_function(array($this,'end'),0,false); $status = $this->processRequest(); $this->afterRequest(); return $status; From f65a0bd9acc946ab068152a31d7783ce0b47ef4a Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Mon, 11 Feb 2013 16:34:02 -0500 Subject: [PATCH 11/84] url wip --- framework/web/UrlRule.php | 94 ++++++++++++++++++++++++++++++++--------------- 1 file changed, 64 insertions(+), 30 deletions(-) diff --git a/framework/web/UrlRule.php b/framework/web/UrlRule.php index 612a793..f11fd16 100644 --- a/framework/web/UrlRule.php +++ b/framework/web/UrlRule.php @@ -33,40 +33,66 @@ class UrlRule extends Object * will be injected into $_GET. */ public $defaults = array(); + /** + * @var boolean whether this rule is only used for request parsing. + * Defaults to false, meaning the rule is used for both URL parsing and creation. + */ + public $parsingOnly = false; + /** + * @var string the URL suffix used for this rule. + * For example, ".html" can be used so that the URL looks like pointing to a static HTML page. + * If not, the value of [[UrlManager::suffix]] will be used. + */ + public $suffix; + /** + * @var string|array the HTTP verb (e.g. GET, POST, DELETE) that this rule should match. + * Use array to represent multiple verbs that this rule may match. + * If this property is not set, the rule can match any verb. + * Note that this property is only used when parsing a request. It is ignored for URL creation. + * @see parsingOnly + */ + public $verb; + /** + * @var string the host info (e.g. `http://www.example.com`) that this rule should match. + * If not set, it means the host info is ignored. + */ + public $hostInfo; /** * @var string the template for generating a new URL. This is derived from [[pattern]] and is used in generating URL. */ - protected $template; + private $_template; /** * @var string the regex for matching the route part. This is used in generating URL. */ - protected $routeRule; + private $_routeRule; /** * @var array list of regex for matching parameters. This is used in generating URL. */ - protected $paramRules = array(); + private $_paramRules = array(); /** * @var array list of parameters used in the route. */ - protected $routeParams = array(); + private $_routeParams = array(); /** * Initializes this rule. */ public function init() { - $this->compileRule(); - } + if ($this->verb !== null) { + if (is_array($this->verb)) { + foreach ($this->verb as $i => $verb) { + $this->verb[$i] = strtoupper($verb); + } + } else { + $this->verb = array(strtoupper($this->verb)); + } + } - /** - * Compiles the rule using the current configuration. - */ - protected function compileRule() - { $this->pattern = trim($this->pattern, '/'); if ($this->pattern === '') { - $this->template = ''; + $this->_template = ''; $this->pattern = '#^$#u'; return; } else { @@ -76,7 +102,7 @@ class UrlRule extends Object $this->route = trim($this->route, '/'); if (strpos($this->route, '<') !== false && preg_match_all('/<(\w+)>/', $this->route, $matches)) { foreach ($matches[1] as $name) { - $this->routeParams[$name] = "<$name>"; + $this->_routeParams[$name] = "<$name>"; } } @@ -96,24 +122,28 @@ class UrlRule extends Object } else { $tr["<$name>"] = "(?P<$name>$pattern)"; } - if (isset($this->routeParams[$name])) { + if (isset($this->_routeParams[$name])) { $tr2["<$name>"] = "(?P<$name>$pattern)"; } else { - $this->paramRules[$name] = $pattern === '[^\/]+' ? '' : "#^$pattern$#"; + $this->_paramRules[$name] = $pattern === '[^\/]+' ? '' : "#^$pattern$#"; } } } - $this->template = preg_replace('/<(\w+):?([^>]+)?>/', '<$1>', $this->pattern); - $this->pattern = '#^' . trim(strtr($this->template, $tr), '/') . '$#u'; + $this->_template = preg_replace('/<(\w+):?([^>]+)?>/', '<$1>', $this->pattern); + $this->pattern = '#^' . trim(strtr($this->_template, $tr), '/') . '$#u'; - if ($this->routeParams !== array()) { - $this->routeRule = '#^' . strtr($this->route, $tr2) . '$#u'; + if ($this->_routeParams !== array()) { + $this->_routeRule = '#^' . strtr($this->route, $tr2) . '$#u'; } } public function parseUrl($pathInfo) { + if ($this->verb !== null && !in_array(\Yii::$app->getRequest()->verb, $this->verb, true)) { + return false; + } + if (!preg_match($this->pattern, $pathInfo, $matches)) { return false; } @@ -125,14 +155,14 @@ class UrlRule extends Object $params = $this->defaults; $tr = array(); foreach ($matches as $name => $value) { - if (isset($this->routeParams[$name])) { - $tr[$this->routeParams[$name]] = $value; + if (isset($this->_routeParams[$name])) { + $tr[$this->_routeParams[$name]] = $value; unset($params[$name]); - } elseif (isset($this->paramRules[$name])) { + } elseif (isset($this->_paramRules[$name])) { $params[$name] = $value; } } - if ($this->routeRule !== null) { + if ($this->_routeRule !== null) { $route = strtr($this->route, $tr); } else { $route = $this->route; @@ -142,12 +172,16 @@ class UrlRule extends Object public function createUrl($route, $params) { + if ($this->parsingOnly) { + return false; + } + $tr = array(); // match the route part first if ($route !== $this->route) { - if ($this->routeRule !== null && preg_match($this->routeRule, $route, $matches)) { - foreach ($this->routeParams as $name => $token) { + if ($this->_routeRule !== null && preg_match($this->_routeRule, $route, $matches)) { + foreach ($this->_routeParams as $name => $token) { if (isset($this->defaults[$name]) && strcmp($this->defaults[$name], $matches[$name]) === 0) { $tr[$token] = ''; } else { @@ -162,23 +196,23 @@ class UrlRule extends Object // match default params // if a default param is not in the route pattern, its value must also be matched foreach ($this->defaults as $name => $value) { - if (isset($this->routeParams[$name])) { + if (isset($this->_routeParams[$name])) { continue; } if (!isset($params[$name])) { return false; } elseif (strcmp($params[$name], $value) === 0) { // strcmp will do string conversion automatically unset($params[$name]); - if (isset($this->paramRules[$name])) { + if (isset($this->_paramRules[$name])) { $tr["<$name>"] = ''; } - } elseif (!isset($this->paramRules[$name])) { + } elseif (!isset($this->_paramRules[$name])) { return false; } } // match params in the pattern - foreach ($this->paramRules as $name => $rule) { + foreach ($this->_paramRules as $name => $rule) { if (isset($params[$name]) && ($rule === '' || preg_match($rule, $params[$name]))) { $tr["<$name>"] = urlencode($params[$name]); unset($params[$name]); @@ -187,7 +221,7 @@ class UrlRule extends Object } } - $url = trim(strtr($this->template, $tr), '/'); + $url = trim(strtr($this->_template, $tr), '/'); if (strpos($url, '//') !== false) { $url = preg_replace('#/+#', '/', $url); } From 12fbb0f71d2357df5d4ad776160e3b34fafeb8f7 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Mon, 11 Feb 2013 19:51:18 -0500 Subject: [PATCH 12/84] url WIP --- framework/web/UrlManager.php | 20 ++++- framework/web/UrlRule.php | 67 +++++++++++++--- tests/unit/framework/web/UrlRuleTest.php | 133 ++++++++++++++++++++++++++++++- 3 files changed, 207 insertions(+), 13 deletions(-) diff --git a/framework/web/UrlManager.php b/framework/web/UrlManager.php index e03f086..8f927e8 100644 --- a/framework/web/UrlManager.php +++ b/framework/web/UrlManager.php @@ -27,7 +27,7 @@ class UrlManager extends Component * @var string the URL suffix used when in 'path' format. * For example, ".html" can be used so that the URL looks like pointing to a static HTML page. Defaults to empty. */ - public $urlSuffix = ''; + public $suffix; /** * @var boolean whether to show entry script name in the constructed URL. Defaults to true. */ @@ -122,7 +122,7 @@ class UrlManager extends Component /** @var $rule UrlRule */ foreach ($this->rules as $rule) { - if (($url = $rule->createUrl($route, $params)) !== false) { + if (($url = $rule->createUrl($this, $route, $params)) !== false) { return $this->getBaseUrl() . $url . $anchor; } } @@ -144,4 +144,20 @@ class UrlManager extends Component { $this->_baseUrl = trim($value, '/'); } + + /** + * Removes the URL suffix from path info. + * @param string $pathInfo path info part in the URL + * @param string $suffix the URL suffix to be removed + * @return string path info with URL suffix removed. + */ + public function removeSuffix($pathInfo, $suffix) + { + $n = strlen($suffix); + if ($n > 0 && substr($pathInfo, -$n) === $suffix) { + return substr($pathInfo, 0, -$n); + } else { + return $pathInfo; + } + } } diff --git a/framework/web/UrlRule.php b/framework/web/UrlRule.php index f11fd16..471712b 100644 --- a/framework/web/UrlRule.php +++ b/framework/web/UrlRule.php @@ -20,6 +20,15 @@ use yii\base\Object; class UrlRule extends Object { /** + * Set [[mode]] with this value to mark that this rule is for URL parsing only + */ + const PARSING_ONLY = 1; + /** + * Set [[mode]] with this value to mark that this rule is for URL creation only + */ + const CREATION_ONLY = 2; + + /** * @var string regular expression used to parse a URL */ public $pattern; @@ -34,11 +43,6 @@ class UrlRule extends Object */ public $defaults = array(); /** - * @var boolean whether this rule is only used for request parsing. - * Defaults to false, meaning the rule is used for both URL parsing and creation. - */ - public $parsingOnly = false; - /** * @var string the URL suffix used for this rule. * For example, ".html" can be used so that the URL looks like pointing to a static HTML page. * If not, the value of [[UrlManager::suffix]] will be used. @@ -49,7 +53,6 @@ class UrlRule extends Object * Use array to represent multiple verbs that this rule may match. * If this property is not set, the rule can match any verb. * Note that this property is only used when parsing a request. It is ignored for URL creation. - * @see parsingOnly */ public $verb; /** @@ -57,6 +60,14 @@ class UrlRule extends Object * If not set, it means the host info is ignored. */ public $hostInfo; + /** + * @var integer a value indicating if this rule should be used for both URL parsing and creation, + * parsing only, or creation only. + * If not set, it means the rule is both URL parsing and creation. + * If it is [[PARSING_ONLY]], the rule is for URL parsing only. + * If it is [[CREATION_ONLY]], the rule is for URL creation only. + */ + public $mode; /** * @var string the template for generating a new URL. This is derived from [[pattern]] and is used in generating URL. @@ -138,12 +149,38 @@ class UrlRule extends Object } } - public function parseUrl($pathInfo) + /** + * Parses the given path info and returns the corresponding route and parameters. + * @param UrlManager $manager the URL manager + * @param string $pathInfo the path info to be parsed. It should not have slashes at the beginning or the end. + * @return array|boolean the parsing result. The route and the parameters are returned as an array. + * If false, it means this rule cannot be used to parse this path info. + */ + public function parseUrl($manager, $pathInfo) { + if ($this->mode === self::CREATION_ONLY) { + return false; + } + if ($this->verb !== null && !in_array(\Yii::$app->getRequest()->verb, $this->verb, true)) { return false; } + $suffix = (string)($this->suffix === null ? $manager->suffix : $this->suffix); + if ($suffix !== '' && $pathInfo !== '') { + $n = strlen($suffix); + if (substr($pathInfo, -$n) === $suffix) { + $pathInfo = substr($pathInfo, 0, -$n); + if ($pathInfo === '') { + // suffix alone is not allowed + return false; + } + } elseif ($suffix !== '/') { + // we allow the ending '/' to be optional if it is a suffix + return false; + } + } + if (!preg_match($this->pattern, $pathInfo, $matches)) { return false; } @@ -170,9 +207,16 @@ class UrlRule extends Object return array($route, $params); } - public function createUrl($route, $params) + /** + * Creates a URL according to the given route and parameters. + * @param UrlManager $manager the URL manager + * @param string $route the route. It should not have slashes at the beginning or the end. + * @param array $params the parameters + * @return string|boolean the created URL, or false if this rule cannot be used for creating this URL. + */ + public function createUrl($manager, $route, $params) { - if ($this->parsingOnly) { + if ($this->mode === self::PARSING_ONLY) { return false; } @@ -225,6 +269,11 @@ class UrlRule extends Object if (strpos($url, '//') !== false) { $url = preg_replace('#/+#', '/', $url); } + + if ($url !== '') { + $url .= ($this->suffix === null ? $manager->suffix : $this->suffix); + } + if ($params !== array()) { $url .= '?' . http_build_query($params); } diff --git a/tests/unit/framework/web/UrlRuleTest.php b/tests/unit/framework/web/UrlRuleTest.php index e9148c7..fd9a8bd 100644 --- a/tests/unit/framework/web/UrlRuleTest.php +++ b/tests/unit/framework/web/UrlRuleTest.php @@ -2,19 +2,21 @@ namespace yiiunit\framework\web; +use yii\web\UrlManager; use yii\web\UrlRule; class UrlRuleTest extends \yiiunit\TestCase { public function testCreateUrl() { + $manager = new UrlManager; $suites = $this->getTestsForCreateUrl(); foreach ($suites as $i => $suite) { list ($name, $config, $tests) = $suite; $rule = new UrlRule($config); foreach ($tests as $j => $test) { list ($route, $params, $expected) = $test; - $url = $rule->createUrl($route, $params); + $url = $rule->createUrl($manager, $route, $params); $this->assertEquals($expected, $url, "Test#$i-$j: $name"); } } @@ -22,6 +24,7 @@ class UrlRuleTest extends \yiiunit\TestCase public function testParseUrl() { + $manager = new UrlManager; $suites = $this->getTestsForParseUrl(); foreach ($suites as $i => $suite) { list ($name, $config, $tests) = $suite; @@ -30,7 +33,7 @@ class UrlRuleTest extends \yiiunit\TestCase $pathInfo = $test[0]; $route = $test[1]; $params = isset($test[2]) ? $test[2] : array(); - $result = $rule->parseUrl($pathInfo); + $result = $rule->parseUrl($manager, $pathInfo); if ($route === false) { $this->assertFalse($result, "Test#$i-$j: $name"); } else { @@ -75,6 +78,17 @@ class UrlRuleTest extends \yiiunit\TestCase ), ), array( + 'parsing only', + array( + 'pattern' => 'posts', + 'route' => 'post/index', + 'mode' => UrlRule::PARSING_ONLY, + ), + array( + array('post/index', array(), false), + ), + ), + array( 'with param', array( 'pattern' => 'post/', @@ -259,6 +273,58 @@ class UrlRuleTest extends \yiiunit\TestCase array('post/index', array('page' => 1), 'post?page=1'), ), ), + array( + 'empty pattern with suffix', + array( + 'pattern' => '', + 'route' => 'post/index', + 'suffix' => '.html', + ), + array( + array('post/index', array(), ''), + array('comment/index', array(), false), + array('post/index', array('page' => 1), '?page=1'), + ), + ), + array( + 'regular pattern with suffix', + array( + 'pattern' => 'posts', + 'route' => 'post/index', + 'suffix' => '.html', + ), + array( + array('post/index', array(), 'posts.html'), + array('comment/index', array(), false), + array('post/index', array('page' => 1), 'posts.html?page=1'), + ), + ), + array( + 'empty pattern with slash suffix', + array( + 'pattern' => '', + 'route' => 'post/index', + 'suffix' => '/', + ), + array( + array('post/index', array(), ''), + array('comment/index', array(), false), + array('post/index', array('page' => 1), '?page=1'), + ), + ), + array( + 'regular pattern with slash suffix', + array( + 'pattern' => 'posts', + 'route' => 'post/index', + 'suffix' => '/', + ), + array( + array('post/index', array(), 'posts/'), + array('comment/index', array(), false), + array('post/index', array('page' => 1), 'posts/?page=1'), + ), + ), ); } @@ -295,6 +361,17 @@ class UrlRuleTest extends \yiiunit\TestCase ), ), array( + 'creation only', + array( + 'pattern' => 'posts', + 'route' => 'post/index', + 'mode' => UrlRule::CREATION_ONLY, + ), + array( + array('posts', false), + ), + ), + array( 'with param', array( 'pattern' => 'post/', @@ -479,6 +556,58 @@ class UrlRuleTest extends \yiiunit\TestCase array('index', false), ), ), + array( + 'empty pattern with suffix', + array( + 'pattern' => '', + 'route' => 'post/index', + 'suffix' => '.html', + ), + array( + array('', 'post/index'), + array('.html', false), + array('a.html', false), + ), + ), + array( + 'regular pattern with suffix', + array( + 'pattern' => 'posts', + 'route' => 'post/index', + 'suffix' => '.html', + ), + array( + array('posts.html', 'post/index'), + array('posts', false), + array('posts.HTML', false), + array('a.html', false), + array('a', false), + ), + ), + array( + 'empty pattern with slash suffix', + array( + 'pattern' => '', + 'route' => 'post/index', + 'suffix' => '/', + ), + array( + array('', 'post/index'), + array('a', false), + ), + ), + array( + 'regular pattern with slash suffix', + array( + 'pattern' => 'posts', + 'route' => 'post/index', + 'suffix' => '/', + ), + array( + array('posts', 'post/index'), + array('a', false), + ), + ), ); } } From 2d5db95149ed711ceca24144aa2208cd4561674b Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Wed, 13 Feb 2013 03:51:29 +0400 Subject: [PATCH 13/84] More on error handling: - Correct exit code for require errors in CLI mode. It was 0 even in case of error. - Solved handling fatals and displaying custom error template w/o output buffering. - If XDEBUG is available, error screen will display trace (implementation inspired by Kohana and Nette). - Added ErrorException to store/display type of the error and user-friendly messages. - Fatals are still logged in PHP log. - Turned off native display_errors since we're now catching all errors. - Added reserving memory (256kb) . Can be removed safely. In case of removal we're losing only memory exhausted error when allocating last very small chunk of memory. - Properly handled errors in __toString (exception can't be thrown in this case). - In the YII_DEBUG===false mode it's bad to display exception name even in page title. - In the YII_DEBUG===true mode it's still useful to get user-friendly message additionally to exception name. --- framework/base/Application.php | 88 ++++++++++++++++++++++++++++++--------- framework/base/ErrorException.php | 47 +++++++++++++++++++++ framework/base/Exception.php | 3 +- framework/views/error.php | 7 ++-- framework/views/exception.php | 5 ++- 5 files changed, 123 insertions(+), 27 deletions(-) create mode 100644 framework/base/ErrorException.php diff --git a/framework/base/Application.php b/framework/base/Application.php index b0167df..7c06e79 100644 --- a/framework/base/Application.php +++ b/framework/base/Application.php @@ -97,6 +97,12 @@ class Application extends Module private $_language; /** + * @var string Used to reserve memory for fatal error handler. This memory + * reserve can be removed if it's OK to write to PHP log only in this particular case. + */ + private $_memoryReserve; + + /** * Constructor. * @param string $id the ID of this application. The ID should uniquely identify the application from others. * @param string $basePath the base path of this application. This should point to @@ -110,6 +116,7 @@ class Application extends Module $this->setBasePath($basePath); if (YII_ENABLE_ERROR_HANDLER) { + ini_set('display_errors', 0); set_exception_handler(array($this, 'handleException')); set_error_handler(array($this, 'handleError'), error_reporting()); } @@ -126,7 +133,6 @@ class Application extends Module */ public function init() { - ob_start(); $this->preloadComponents(); } @@ -139,27 +145,54 @@ class Application extends Module */ public function end($status = 0, $exit = true) { - $lastError = error_get_last(); - - if(isset($lastError['type']) && in_array($lastError['type'], array(E_ERROR, E_PARSE))) { - ob_end_clean(); - $exception = new \ErrorException($lastError['message'], 0, $lastError['type'], $lastError['file'], $lastError['line']); - $this->logException($exception); - - if (($handler = $this->getErrorHandler()) !== null) { - $handler->handle($exception); - } else { - $this->renderException($exception); - } - - die(1); - } - if (!$this->_ended) { $this->_ended = true; $this->afterRequest(); } - ob_end_flush(); + + if(YII_ENABLE_ERROR_HANDLER) { + $error = error_get_last(); + + if(isset($error['type']) && in_array($error['type'], ErrorException::getFatalCodes())) { + unset($this->_memoryReserve); + $exception = new ErrorException($error['message'], $error['type'], $error['type'], $error['file'], $error['line']); + + if(function_exists('xdebug_get_function_stack')) { + $trace = array_slice(array_reverse(xdebug_get_function_stack()), 4, -1); + foreach($trace as &$frame) { + if(!isset($frame['function'])) { + $frame['function'] = 'unknown'; + } + + // XDebug < 2.1.1: http://bugs.xdebug.org/view.php?id=695 + if(!isset($frame['type'])) { + $frame['type'] = '::'; + } + + // XDebug has a different key name + $frame['args'] = array(); + if(isset($frame['params']) && !isset($frame['args'])) { + $frame['args'] = $frame['params']; + } + } + + $ref = new \ReflectionProperty('Exception', 'trace'); + $ref->setAccessible(true); + $ref->setValue($exception, $trace); + } + + $this->logException($exception); + + if (($handler = $this->getErrorHandler()) !== null) { + $handler->handle($exception); + } else { + $this->renderException($exception); + } + + $status = 1; + } + } + if ($exit) { exit($status); } @@ -173,6 +206,9 @@ class Application extends Module public function run() { $this->beforeRequest(); + // Allocating twice more than required to display memory exhausted error + // in case of trying to allocate last 1 byte while all memory is taken. + $this->_memoryReserve = str_repeat('x', 1024*256); register_shutdown_function(array($this,'end'),0,false); $status = $this->processRequest(); $this->afterRequest(); @@ -394,12 +430,24 @@ class Application extends Module * @param string $message the error message * @param string $file the filename that the error was raised in * @param integer $line the line number the error was raised at - * @throws \ErrorException the error exception + * + * @throws ErrorException */ public function handleError($code, $message, $file, $line) { if (error_reporting() !== 0) { - throw new \ErrorException($message, 0, $code, $file, $line); + $exception = new ErrorException($message, $code, $code, $file, $line); + + // in case error appeared in __toString method we can't throw any exception + $trace = debug_backtrace(false); + array_shift($trace); + foreach($trace as $frame) { + if($frame['function'] == '__toString') { + $this->handleException($exception); + } + } + + throw $exception; } } diff --git a/framework/base/ErrorException.php b/framework/base/ErrorException.php new file mode 100644 index 0000000..073c1c3 --- /dev/null +++ b/framework/base/ErrorException.php @@ -0,0 +1,47 @@ + + * @since 2.0 + */ +class ErrorException extends \ErrorException +{ + public static function getFatalCodes() + { + return array(E_ERROR, E_PARSE, E_CORE_ERROR, E_CORE_WARNING, E_COMPILE_ERROR, E_COMPILE_WARNING); + } + + /** + * @return string the user-friendly name of this exception + */ + public function getName() + { + $names = array( + E_ERROR => \Yii::t('yii|Fatal Error'), + E_PARSE => \Yii::t('yii|Parse Error'), + E_CORE_ERROR => \Yii::t('yii|Core Error'), + E_COMPILE_ERROR => \Yii::t('yii|Compile Error'), + E_USER_ERROR => \Yii::t('yii|User Error'), + E_WARNING => \Yii::t('yii|Warning'), + E_CORE_WARNING => \Yii::t('yii|Core Warning'), + E_COMPILE_WARNING => \Yii::t('yii|Compile Warning'), + E_USER_WARNING => \Yii::t('yii|User Warning'), + E_STRICT => \Yii::t('yii|Strict'), + E_NOTICE => \Yii::t('yii|Notice'), + E_RECOVERABLE_ERROR => \Yii::t('yii|Recoverable Error'), + E_DEPRECATED => \Yii::t('yii|Deprecated'), + ); + return isset($names[$this->getCode()]) ? $names[$this->getCode()] : \Yii::t('yii|Error'); + } +} diff --git a/framework/base/Exception.php b/framework/base/Exception.php index 0088a55..7dcd7c5 100644 --- a/framework/base/Exception.php +++ b/framework/base/Exception.php @@ -24,5 +24,4 @@ class Exception extends \Exception { return \Yii::t('yii|Exception'); } -} - +} \ No newline at end of file diff --git a/framework/views/error.php b/framework/views/error.php index c1700f2..28c50f1 100644 --- a/framework/views/error.php +++ b/framework/views/error.php @@ -4,12 +4,13 @@ * @var \yii\base\ErrorHandler $owner */ $owner = $this->owner; +$title = $owner->htmlEncode($exception instanceof \yii\base\Exception || $exception instanceof \yii\base\ErrorException ? $exception->getName() : get_class($exception)); ?> - <?php echo get_class($exception)?> + <?php echo $title?> "; + } + + /** + * Registers a 'refresh' meta tag. + * This method can be invoked anywhere in a view. It will register a 'refresh' + * meta tag with {@link CClientScript} so that the page can be refreshed in + * the specified seconds. + * @param integer $seconds the number of seconds to wait before refreshing the page + * @param string $url the URL to which the page should be redirected to. If empty, it means the current page. + * @since 1.1.1 + */ + public static function refresh($seconds, $url = '') + { + $content = "$seconds"; + if ($url !== '') { + $content .= ';' . static::normalizeUrl($url); + } + Yii::app()->clientScript->registerMetaTag($content, null, 'refresh'); + } + + /** + * Links to the specified CSS file. + * @param string $url the CSS URL + * @param string $media the media that this CSS should apply to. + * @return string the CSS link. + */ + public static function cssFile($url, $media = '') + { + return CHtml::linkTag('stylesheet', 'text/css', $url, $media !== '' ? $media : null); + } + + /** + * Encloses the given JavaScript within a script tag. + * @param string $text the JavaScript to be enclosed + * @return string the enclosed JavaScript + */ + public static function script($text) + { + return ""; + } + + /** + * Includes a JavaScript file. + * @param string $url URL for the JavaScript file + * @return string the JavaScript file tag + */ + public static function scriptFile($url) + { + return ''; + } + + /** + * Generates an opening form tag. + * This is a shortcut to {@link beginForm}. + * @param mixed $action the form action URL (see {@link normalizeUrl} for details about this parameter.) + * @param string $method form method (e.g. post, get) + * @param array $htmlOptions additional HTML attributes (see {@link tag}). + * @return string the generated form tag. + */ + public static function form($action = '', $method = 'post', $htmlOptions = array()) + { + return static::beginForm($action, $method, $htmlOptions); + } + + /** + * Generates an opening form tag. + * Note, only the open tag is generated. A close tag should be placed manually + * at the end of the form. + * @param mixed $action the form action URL (see {@link normalizeUrl} for details about this parameter.) + * @param string $method form method (e.g. post, get) + * @param array $htmlOptions additional HTML attributes (see {@link tag}). + * @return string the generated form tag. + * @see endForm + */ + public static function beginForm($action = '', $method = 'post', $htmlOptions = array()) + { + $htmlOptions['action'] = $url = static::normalizeUrl($action); + $htmlOptions['method'] = $method; + $form = static::tag('form', $htmlOptions, false, false); + $hiddens = array(); + if (!strcasecmp($method, 'get') && ($pos = strpos($url, '?')) !== false) { + foreach (explode('&', substr($url, $pos + 1)) as $pair) { + if (($pos = strpos($pair, '=')) !== false) { + $hiddens[] = static::hiddenField(urldecode(substr($pair, 0, $pos)), urldecode(substr($pair, $pos + 1)), array('id' => false)); + } else { + $hiddens[] = static::hiddenField(urldecode($pair), '', array('id' => false)); + } + } + } + $request = Yii::app()->request; + if ($request->enableCsrfValidation && !strcasecmp($method, 'post')) { + $hiddens[] = static::hiddenField($request->csrfTokenName, $request->getCsrfToken(), array('id' => false)); + } + if ($hiddens !== array()) { + $form .= "\n" . static::tag('div', array('style' => 'display:none'), implode("\n", $hiddens)); + } + return $form; + } + + /** + * Generates a closing form 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. + * @param mixed $url a URL or an action route that can be used to create a URL. + * See {@link normalizeUrl} for more details about how to specify this parameter. + * @param array $htmlOptions additional HTML attributes. Besides normal HTML attributes, a few special + * attributes are also recognized (see {@link clientChange} and {@link tag} for more details.) + * @return string the generated hyperlink + * @see normalizeUrl + * @see clientChange + */ + public static function link($text, $url = '#', $htmlOptions = array()) + { + if ($url !== '') { + $htmlOptions['href'] = static::normalizeUrl($url); + } + static::clientChange('click', $htmlOptions); + return static::tag('a', $htmlOptions, $text); + } + + /** + * Generates a mailto link. + * @param string $text link body. It will NOT be HTML-encoded. Therefore you can pass in HTML code such as an image tag. + * @param string $email email address. If this is empty, the first parameter (link body) will be treated as the email address. + * @param array $htmlOptions additional HTML attributes. Besides normal HTML attributes, a few special + * attributes are also recognized (see {@link clientChange} and {@link tag} for more details.) + * @return string the generated mailto link + * @see clientChange + */ + public static function mailto($text, $email = '', $htmlOptions = array()) + { + if ($email === '') { + $email = $text; + } + return static::link($text, 'mailto:' . $email, $htmlOptions); + } + + /** + * Generates an image tag. + * @param string $src the image URL + * @param string $alt the alternative text display + * @param array $htmlOptions additional HTML attributes (see {@link tag}). + * @return string the generated image tag + */ + public static function image($src, $alt = '', $htmlOptions = array()) + { + $htmlOptions['src'] = $src; + $htmlOptions['alt'] = $alt; + return static::tag('img', $htmlOptions); + } + + /** + * Generates a button. + * @param string $label the button label + * @param array $htmlOptions additional HTML attributes. Besides normal HTML attributes, a few special + * attributes are also recognized (see {@link clientChange} and {@link tag} for more details.) + * @return string the generated button tag + * @see clientChange + */ + public static function button($name, $label = 'button', $htmlOptions = array()) + { + if (!isset($htmlOptions['name'])) { + if (!array_key_exists('name', $htmlOptions)) { + $htmlOptions['name'] = static::ID_PREFIX . static::$count++; + } + } + if (!isset($htmlOptions['type'])) { + $htmlOptions['type'] = 'button'; + } + if (!isset($htmlOptions['value'])) { + $htmlOptions['value'] = $label; + } + static::clientChange('click', $htmlOptions); + return static::tag('input', $htmlOptions); + } + + /** + * Generates a button using HTML button tag. + * This method is similar to {@link button} except that it generates a 'button' + * tag instead of 'input' tag. + * @param string $label the button label. Note that this value will be directly inserted in the button element + * without being HTML-encoded. + * @param array $htmlOptions additional HTML attributes. Besides normal HTML attributes, a few special + * attributes are also recognized (see {@link clientChange} and {@link tag} for more details.) + * @return string the generated button tag + * @see clientChange + */ + public static function htmlButton($label = 'button', $htmlOptions = array()) + { + if (!isset($htmlOptions['name'])) { + $htmlOptions['name'] = static::ID_PREFIX . static::$count++; + } + if (!isset($htmlOptions['type'])) { + $htmlOptions['type'] = 'button'; + } + static::clientChange('click', $htmlOptions); + return static::tag('button', $htmlOptions, $label); + } + + /** + * Generates a submit button. + * @param string $label the button label + * @param array $htmlOptions additional HTML attributes. Besides normal HTML attributes, a few special + * attributes are also recognized (see {@link clientChange} and {@link tag} for more details.) + * @return string the generated button tag + * @see clientChange + */ + public static function submitButton($label = 'submit', $htmlOptions = array()) + { + $htmlOptions['type'] = 'submit'; + return static::button($label, $htmlOptions); + } + + /** + * Generates a reset button. + * @param string $label the button label + * @param array $htmlOptions additional HTML attributes. Besides normal HTML attributes, a few special + * attributes are also recognized (see {@link clientChange} and {@link tag} for more details.) + * @return string the generated button tag + * @see clientChange + */ + public static function resetButton($label = 'reset', $htmlOptions = array()) + { + $htmlOptions['type'] = 'reset'; + return static::button($label, $htmlOptions); + } + + /** + * Generates an image submit button. + * @param string $src the image URL + * @param array $htmlOptions additional HTML attributes. Besides normal HTML attributes, a few special + * attributes are also recognized (see {@link clientChange} and {@link tag} for more details.) + * @return string the generated button tag + * @see clientChange + */ + public static function imageButton($src, $htmlOptions = array()) + { + $htmlOptions['src'] = $src; + $htmlOptions['type'] = 'image'; + return static::button('submit', $htmlOptions); + } + + /** + * Generates a link submit button. + * @param string $label the button label + * @param array $htmlOptions additional HTML attributes. Besides normal HTML attributes, a few special + * attributes are also recognized (see {@link clientChange} and {@link tag} for more details.) + * @return string the generated button tag + * @see clientChange + */ + public static function linkButton($label = 'submit', $htmlOptions = array()) + { + if (!isset($htmlOptions['submit'])) { + $htmlOptions['submit'] = isset($htmlOptions['href']) ? $htmlOptions['href'] : ''; + } + return static::link($label, '#', $htmlOptions); + } + + /** + * Generates a label tag. + * @param string $label label text. Note, you should HTML-encode the text if needed. + * @param string $for the ID of the HTML element that this label is associated with. + * If this is false, the 'for' attribute for the label tag will not be rendered. + * @param array $htmlOptions additional HTML attributes. + * The following HTML option is recognized: + *
    + *
  • required: if this is set and is true, the label will be styled + * with CSS class 'required' (customizable with CHtml::$requiredCss), + * and be decorated with {@link CHtml::beforeRequiredLabel} and + * {@link CHtml::afterRequiredLabel}.
  • + *
+ * @return string the generated label tag + */ + public static function label($label, $for, $htmlOptions = array()) + { + if ($for === false) { + unset($htmlOptions['for']); + } else { + $htmlOptions['for'] = $for; + } + if (isset($htmlOptions['required'])) { + if ($htmlOptions['required']) { + if (isset($htmlOptions['class'])) { + $htmlOptions['class'] .= ' ' . static::$requiredCss; + } else { + $htmlOptions['class'] = static::$requiredCss; + } + $label = static::$beforeRequiredLabel . $label . static::$afterRequiredLabel; + } + unset($htmlOptions['required']); + } + return static::tag('label', $htmlOptions, $label); + } + + /** + * Generates a text field input. + * @param string $name the input name + * @param string $value the input value + * @param array $htmlOptions additional HTML attributes. Besides normal HTML attributes, a few special + * attributes are also recognized (see {@link clientChange} and {@link tag} for more details.) + * @return string the generated input field + * @see clientChange + * @see inputField + */ + public static function textField($name, $value = '', $htmlOptions = array()) + { + static::clientChange('change', $htmlOptions); + return static::inputField('text', $name, $value, $htmlOptions); + } + + /** + * Generates a hidden input. + * @param string $name the input name + * @param string $value the input value + * @param array $htmlOptions additional HTML attributes (see {@link tag}). + * @return string the generated input field + * @see inputField + */ + public static function hiddenField($name, $value = '', $htmlOptions = array()) + { + return static::inputField('hidden', $name, $value, $htmlOptions); + } + + /** + * Generates a password field input. + * @param string $name the input name + * @param string $value the input value + * @param array $htmlOptions additional HTML attributes. Besides normal HTML attributes, a few special + * attributes are also recognized (see {@link clientChange} and {@link tag} for more details.) + * @return string the generated input field + * @see clientChange + * @see inputField + */ + public static function passwordField($name, $value = '', $htmlOptions = array()) + { + static::clientChange('change', $htmlOptions); + return static::inputField('password', $name, $value, $htmlOptions); + } + + /** + * Generates a file input. + * Note, you have to 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 input name + * @param string $value the input value + * @param array $htmlOptions additional HTML attributes (see {@link tag}). + * @return string the generated input field + * @see inputField + */ + public static function fileField($name, $value = '', $htmlOptions = array()) + { + return static::inputField('file', $name, $value, $htmlOptions); + } + + /** + * Generates a text area input. + * @param string $name the input name + * @param string $value the input value + * @param array $htmlOptions additional HTML attributes. Besides normal HTML attributes, a few special + * attributes are also recognized (see {@link clientChange} and {@link tag} for more details.) + * @return string the generated text area + * @see clientChange + * @see inputField + */ + public static function textArea($name, $value = '', $htmlOptions = array()) + { + $htmlOptions['name'] = $name; + if (!isset($htmlOptions['id'])) { + $htmlOptions['id'] = static::getIdByName($name); + } elseif ($htmlOptions['id'] === false) { + unset($htmlOptions['id']); + } + static::clientChange('change', $htmlOptions); + return static::tag('textarea', $htmlOptions, isset($htmlOptions['encode']) && !$htmlOptions['encode'] ? $value : static::encode($value)); + } + + /** + * Generates a radio button. + * @param string $name the input name + * @param boolean $checked whether the radio button is checked + * @param array $htmlOptions additional HTML attributes. Besides normal HTML attributes, a few special + * attributes are also recognized (see {@link clientChange} and {@link tag} for more details.) + * Since version 1.1.2, a special option named 'uncheckValue' is available that can be used to specify + * the value returned when the radio button is not checked. When set, a hidden field is rendered so that + * when the radio button is not checked, we can still obtain the posted uncheck value. + * If 'uncheckValue' is not set or set to NULL, the hidden field will not be rendered. + * @return string the generated radio button + * @see clientChange + * @see inputField + */ + public static function radioButton($name, $checked = false, $htmlOptions = array()) + { + if ($checked) { + $htmlOptions['checked'] = 'checked'; + } else { + unset($htmlOptions['checked']); + } + $value = isset($htmlOptions['value']) ? $htmlOptions['value'] : 1; + static::clientChange('click', $htmlOptions); + + if (array_key_exists('uncheckValue', $htmlOptions)) { + $uncheck = $htmlOptions['uncheckValue']; + unset($htmlOptions['uncheckValue']); + } else { + $uncheck = null; + } + + if ($uncheck !== null) { + // add a hidden field so that if the radio button is not selected, it still submits a value + if (isset($htmlOptions['id']) && $htmlOptions['id'] !== false) { + $uncheckOptions = array('id' => static::ID_PREFIX . $htmlOptions['id']); + } else { + $uncheckOptions = array('id' => false); + } + $hidden = static::hiddenField($name, $uncheck, $uncheckOptions); + } else { + $hidden = ''; + } + + // add a hidden field so that if the radio button is not selected, it still submits a value + return $hidden . static::inputField('radio', $name, $value, $htmlOptions); + } + + /** + * Generates a check box. + * @param string $name the input name + * @param boolean $checked whether the check box is checked + * @param array $htmlOptions additional HTML attributes. Besides normal HTML attributes, a few special + * attributes are also recognized (see {@link clientChange} and {@link tag} for more details.) + * Since version 1.1.2, a special option named 'uncheckValue' is available that can be used to specify + * the value returned when the checkbox is not checked. When set, a hidden field is rendered so that + * when the checkbox is not checked, we can still obtain the posted uncheck value. + * If 'uncheckValue' is not set or set to NULL, the hidden field will not be rendered. + * @return string the generated check box + * @see clientChange + * @see inputField + */ + public static function checkBox($name, $checked = false, $htmlOptions = array()) + { + if ($checked) { + $htmlOptions['checked'] = 'checked'; + } else { + unset($htmlOptions['checked']); + } + $value = isset($htmlOptions['value']) ? $htmlOptions['value'] : 1; + static::clientChange('click', $htmlOptions); + + if (array_key_exists('uncheckValue', $htmlOptions)) { + $uncheck = $htmlOptions['uncheckValue']; + unset($htmlOptions['uncheckValue']); + } else { + $uncheck = null; + } + + if ($uncheck !== null) { + // add a hidden field so that if the check box is not checked, it still submits a value + if (isset($htmlOptions['id']) && $htmlOptions['id'] !== false) { + $uncheckOptions = array('id' => static::ID_PREFIX . $htmlOptions['id']); + } else { + $uncheckOptions = array('id' => false); + } + $hidden = static::hiddenField($name, $uncheck, $uncheckOptions); + } else { + $hidden = ''; + } + + // add a hidden field so that if the check box is not checked, it still submits a value + return $hidden . static::inputField('checkbox', $name, $value, $htmlOptions); + } + + /** + * Generates a drop down list. + * @param string $name the input name + * @param string $select the selected value + * @param array $data data for generating the list options (value=>display). + * You may use {@link listData} to generate this data. + * Please refer to {@link listOptions} on how this data is used to generate the list options. + * Note, the values and labels will be automatically HTML-encoded by this method. + * @param array $htmlOptions additional HTML attributes. Besides normal HTML attributes, a few special + * attributes are recognized. See {@link clientChange} and {@link tag} for more details. + * In addition, the following options are also supported specifically for dropdown list: + *
    + *
  • encode: boolean, specifies whether to encode the values. Defaults to true.
  • + *
  • prompt: string, specifies the prompt text shown as the first list option. Its value is empty. Note, the prompt text will NOT be HTML-encoded.
  • + *
  • empty: string, specifies the text corresponding to empty selection. Its value is empty. + * The 'empty' option can also be an array of value-label pairs. + * Each pair will be used to render a list option at the beginning. Note, the text label will NOT be HTML-encoded.
  • + *
  • options: array, specifies additional attributes for each OPTION tag. + * The array keys must be the option values, and the array values are the extra + * OPTION tag attributes in the name-value pairs. For example, + *
    +	 *     array(
    +	 *         'value1'=>array('disabled'=>true, 'label'=>'value 1'),
    +	 *         'value2'=>array('label'=>'value 2'),
    +	 *     );
    +	 * 
    + *
  • + *
+ * Since 1.1.13, a special option named 'unselectValue' is available. It can be used to set the value + * that will be returned when no option is selected in multiple mode. When set, a hidden field is + * rendered so that if no option is selected in multiple mode, we can still obtain the posted + * unselect value. If 'unselectValue' is not set or set to NULL, the hidden field will not be rendered. + * @return string the generated drop down list + * @see clientChange + * @see inputField + * @see listData + */ + public static function dropDownList($name, $select, $data, $htmlOptions = array()) + { + $htmlOptions['name'] = $name; + + if (!isset($htmlOptions['id'])) { + $htmlOptions['id'] = static::getIdByName($name); + } elseif ($htmlOptions['id'] === false) { + unset($htmlOptions['id']); + } + + static::clientChange('change', $htmlOptions); + $options = "\n" . static::listOptions($select, $data, $htmlOptions); + $hidden = ''; + + if (isset($htmlOptions['multiple'])) { + if (substr($htmlOptions['name'], -2) !== '[]') { + $htmlOptions['name'] .= '[]'; + } + + if (isset($htmlOptions['unselectValue'])) { + $hiddenOptions = isset($htmlOptions['id']) ? array('id' => static::ID_PREFIX . $htmlOptions['id']) : array('id' => false); + $hidden = static::hiddenField(substr($htmlOptions['name'], 0, -2), $htmlOptions['unselectValue'], $hiddenOptions); + unset($htmlOptions['unselectValue']); + } + } + // add a hidden field so that if the option is not selected, it still submits a value + return $hidden . static::tag('select', $htmlOptions, $options); + } + + /** + * Generates a list box. + * @param string $name the input name + * @param mixed $select the selected value(s). This can be either a string for single selection or an array for multiple selections. + * @param array $data data for generating the list options (value=>display) + * You may use {@link listData} to generate this data. + * Please refer to {@link listOptions} on how this data is used to generate the list options. + * Note, the values and labels will be automatically HTML-encoded by this method. + * @param array $htmlOptions additional HTML attributes. Besides normal HTML attributes, a few special + * attributes are also recognized. See {@link clientChange} and {@link tag} for more details. + * In addition, the following options are also supported specifically for list box: + *
    + *
  • encode: boolean, specifies whether to encode the values. Defaults to true.
  • + *
  • prompt: string, specifies the prompt text shown as the first list option. Its value is empty. Note, the prompt text will NOT be HTML-encoded.
  • + *
  • empty: string, specifies the text corresponding to empty selection. Its value is empty. + * The 'empty' option can also be an array of value-label pairs. + * Each pair will be used to render a list option at the beginning. Note, the text label will NOT be HTML-encoded.
  • + *
  • options: array, specifies additional attributes for each OPTION tag. + * The array keys must be the option values, and the array values are the extra + * OPTION tag attributes in the name-value pairs. For example, + *
    +	 *     array(
    +	 *         'value1'=>array('disabled'=>true, 'label'=>'value 1'),
    +	 *         'value2'=>array('label'=>'value 2'),
    +	 *     );
    +	 * 
    + *
  • + *
+ * @return string the generated list box + * @see clientChange + * @see inputField + * @see listData + */ + public static function listBox($name, $select, $data, $htmlOptions = array()) + { + if (!isset($htmlOptions['size'])) { + $htmlOptions['size'] = 4; + } + if (isset($htmlOptions['multiple'])) { + if (substr($name, -2) !== '[]') { + $name .= '[]'; + } + } + return static::dropDownList($name, $select, $data, $htmlOptions); + } + + /** + * Generates a check box list. + * A check box list allows multiple selection, like {@link listBox}. + * As a result, the corresponding POST value is an array. + * @param string $name name of the check box list. You can use this name to retrieve + * the selected value(s) once the form is submitted. + * @param mixed $select selection of the check boxes. This can be either a string + * for single selection or an array for multiple selections. + * @param array $data value-label pairs used to generate the check box list. + * Note, the values will be automatically HTML-encoded, while the labels will not. + * @param array $htmlOptions additional HTML options. The options will be applied to + * each checkbox input. The following special options are recognized: + *
    + *
  • template: string, specifies how each checkbox is rendered. Defaults + * to "{input} {label}", where "{input}" will be replaced by the generated + * check box input tag while "{label}" be replaced by the corresponding check box label.
  • + *
  • separator: string, specifies the string that separates the generated check boxes.
  • + *
  • checkAll: string, specifies the label for the "check all" checkbox. + * If this option is specified, a 'check all' checkbox will be displayed. Clicking on + * this checkbox will cause all checkboxes checked or unchecked.
  • + *
  • checkAllLast: boolean, specifies whether the 'check all' checkbox should be + * displayed at the end of the checkbox list. If this option is not set (default) + * or is false, the 'check all' checkbox will be displayed at the beginning of + * the checkbox list.
  • + *
  • labelOptions: array, specifies the additional HTML attributes to be rendered + * for every label tag in the list.
  • + *
  • container: string, specifies the checkboxes enclosing tag. Defaults to 'span'. + * If the value is an empty string, no enclosing tag will be generated
  • + *
  • baseID: string, specifies the base ID prefix to be used for checkboxes in the list. + * This option is available since version 1.1.13.
  • + *
+ * @return string the generated check box list + */ + public static function checkBoxList($name, $select, $data, $htmlOptions = array()) + { + $template = isset($htmlOptions['template']) ? $htmlOptions['template'] : '{input} {label}'; + $separator = isset($htmlOptions['separator']) ? $htmlOptions['separator'] : "
\n"; + $container = isset($htmlOptions['container']) ? $htmlOptions['container'] : 'span'; + unset($htmlOptions['template'], $htmlOptions['separator'], $htmlOptions['container']); + + if (substr($name, -2) !== '[]') { + $name .= '[]'; + } + + if (isset($htmlOptions['checkAll'])) { + $checkAllLabel = $htmlOptions['checkAll']; + $checkAllLast = isset($htmlOptions['checkAllLast']) && $htmlOptions['checkAllLast']; + } + unset($htmlOptions['checkAll'], $htmlOptions['checkAllLast']); + + $labelOptions = isset($htmlOptions['labelOptions']) ? $htmlOptions['labelOptions'] : array(); + unset($htmlOptions['labelOptions']); + + $items = array(); + $baseID = isset($htmlOptions['baseID']) ? $htmlOptions['baseID'] : static::getIdByName($name); + unset($htmlOptions['baseID']); + $id = 0; + $checkAll = true; + + foreach ($data as $value => $label) { + $checked = !is_array($select) && !strcmp($value, $select) || is_array($select) && in_array($value, $select); + $checkAll = $checkAll && $checked; + $htmlOptions['value'] = $value; + $htmlOptions['id'] = $baseID . '_' . $id++; + $option = static::checkBox($name, $checked, $htmlOptions); + $label = static::label($label, $htmlOptions['id'], $labelOptions); + $items[] = strtr($template, array('{input}' => $option, '{label}' => $label)); + } + + if (isset($checkAllLabel)) { + $htmlOptions['value'] = 1; + $htmlOptions['id'] = $id = $baseID . '_all'; + $option = static::checkBox($id, $checkAll, $htmlOptions); + $label = static::label($checkAllLabel, $id, $labelOptions); + $item = strtr($template, array('{input}' => $option, '{label}' => $label)); + if ($checkAllLast) { + $items[] = $item; + } else { + array_unshift($items, $item); + } + $name = strtr($name, array('[' => '\\[', ']' => '\\]')); + $js = <<getClientScript(); + $cs->registerCoreScript('jquery'); + $cs->registerScript($id, $js); + } + + if (empty($container)) { + return implode($separator, $items); + } else { + return static::tag($container, array('id' => $baseID), implode($separator, $items)); + } + } + + /** + * Generates a radio button list. + * A radio button list is like a {@link checkBoxList check box list}, except that + * it only allows single selection. + * @param string $name name of the radio button list. You can use this name to retrieve + * the selected value(s) once the form is submitted. + * @param string $select selection of the radio buttons. + * @param array $data value-label pairs used to generate the radio button list. + * Note, the values will be automatically HTML-encoded, while the labels will not. + * @param array $htmlOptions additional HTML options. The options will be applied to + * each radio button input. The following special options are recognized: + *
    + *
  • template: string, specifies how each radio button is rendered. Defaults + * to "{input} {label}", where "{input}" will be replaced by the generated + * radio button input tag while "{label}" will be replaced by the corresponding radio button label.
  • + *
  • separator: string, specifies the string that separates the generated radio buttons. Defaults to new line (
    ).
  • + *
  • labelOptions: array, specifies the additional HTML attributes to be rendered + * for every label tag in the list.
  • + *
  • container: string, specifies the radio buttons enclosing tag. Defaults to 'span'. + * If the value is an empty string, no enclosing tag will be generated
  • + *
  • baseID: string, specifies the base ID prefix to be used for radio buttons in the list. + * This option is available since version 1.1.13.
  • + *
+ * @return string the generated radio button list + */ + public static function radioButtonList($name, $select, $data, $htmlOptions = array()) + { + $template = isset($htmlOptions['template']) ? $htmlOptions['template'] : '{input} {label}'; + $separator = isset($htmlOptions['separator']) ? $htmlOptions['separator'] : "
\n"; + $container = isset($htmlOptions['container']) ? $htmlOptions['container'] : 'span'; + unset($htmlOptions['template'], $htmlOptions['separator'], $htmlOptions['container']); + + $labelOptions = isset($htmlOptions['labelOptions']) ? $htmlOptions['labelOptions'] : array(); + unset($htmlOptions['labelOptions']); + + $items = array(); + $baseID = isset($htmlOptions['baseID']) ? $htmlOptions['baseID'] : static::getIdByName($name); + unset($htmlOptions['baseID']); + $id = 0; + foreach ($data as $value => $label) { + $checked = !strcmp($value, $select); + $htmlOptions['value'] = $value; + $htmlOptions['id'] = $baseID . '_' . $id++; + $option = static::radioButton($name, $checked, $htmlOptions); + $label = static::label($label, $htmlOptions['id'], $labelOptions); + $items[] = strtr($template, array('{input}' => $option, '{label}' => $label)); + } + if (empty($container)) { + return implode($separator, $items); + } else { + return static::tag($container, array('id' => $baseID), implode($separator, $items)); + } + } + + /** + * Normalizes the input parameter to be a valid URL. + * + * If the input parameter is an empty string, the currently requested URL will be returned. + * + * If the input parameter is a non-empty string, it is treated as a valid URL and will + * be returned without any change. + * + * If the input parameter is an array, it is treated as a controller route and a list of + * GET parameters, and the {@link CController::createUrl} method will be invoked to + * create a URL. In this case, the first array element refers to the controller route, + * and the rest key-value pairs refer to the additional GET parameters for the URL. + * For example, array('post/list', 'page'=>3) may be used to generate the URL + * /index.php?r=post/list&page=3. + * + * @param mixed $url the parameter to be used to generate a valid URL + * @return string the normalized URL + */ + public static function normalizeUrl($url) + { + if (is_array($url)) { + if (isset($url[0])) { + if (($c = Yii::app()->getController()) !== null) { + $url = $c->createUrl($url[0], array_splice($url, 1)); + } else { + $url = Yii::app()->createUrl($url[0], array_splice($url, 1)); + } + } else { + $url = ''; + } + } + return $url === '' ? Yii::app()->getRequest()->getUrl() : $url; + } + + /** + * Generates an input HTML tag. + * This method generates an input HTML tag based on the given input name and value. + * @param string $type the input type (e.g. 'text', 'radio') + * @param string $name the input name + * @param string $value the input value + * @param array $htmlOptions additional HTML attributes for the HTML tag (see {@link tag}). + * @return string the generated input tag + */ + protected static function inputField($type, $name, $value, $htmlOptions) + { + $htmlOptions['type'] = $type; + $htmlOptions['value'] = $value; + $htmlOptions['name'] = $name; + if (!isset($htmlOptions['id'])) { + $htmlOptions['id'] = static::getIdByName($name); + } elseif ($htmlOptions['id'] === false) { + unset($htmlOptions['id']); + } + return static::tag('input', $htmlOptions); + } + + /** + * Generates the list options. + * @param mixed $selection the selected value(s). This can be either a string for single selection or an array for multiple selections. + * @param array $listData the option data (see {@link listData}) + * @param array $htmlOptions additional HTML attributes. The following two special attributes are recognized: + *
    + *
  • encode: boolean, specifies whether to encode the values. Defaults to true.
  • + *
  • prompt: string, specifies the prompt text shown as the first list option. Its value is empty. Note, the prompt text will NOT be HTML-encoded.
  • + *
  • empty: string, specifies the text corresponding to empty selection. Its value is empty. + * The 'empty' option can also be an array of value-label pairs. + * Each pair will be used to render a list option at the beginning. Note, the text label will NOT be HTML-encoded.
  • + *
  • options: array, specifies additional attributes for each OPTION tag. + * The array keys must be the option values, and the array values are the extra + * OPTION tag attributes in the name-value pairs. For example, + *
    +	 *     array(
    +	 *         'value1'=>array('disabled'=>true, 'label'=>'value 1'),
    +	 *         'value2'=>array('label'=>'value 2'),
    +	 *     );
    +	 * 
    + *
  • + *
  • key: string, specifies the name of key attribute of the selection object(s). + * This is used when the selection is represented in terms of objects. In this case, + * the property named by the key option of the objects will be treated as the actual selection value. + * This option defaults to 'primaryKey', meaning using the 'primaryKey' property value of the objects in the selection. + * This option has been available since version 1.1.3.
  • + *
+ * @return string the generated list options + */ + public static function listOptions($selection, $listData, &$htmlOptions) + { + $raw = isset($htmlOptions['encode']) && !$htmlOptions['encode']; + $content = ''; + if (isset($htmlOptions['prompt'])) { + $content .= '\n"; + unset($htmlOptions['prompt']); + } + if (isset($htmlOptions['empty'])) { + if (!is_array($htmlOptions['empty'])) { + $htmlOptions['empty'] = array('' => $htmlOptions['empty']); + } + foreach ($htmlOptions['empty'] as $value => $label) { + $content .= '\n"; + } + unset($htmlOptions['empty']); + } + + if (isset($htmlOptions['options'])) { + $options = $htmlOptions['options']; + unset($htmlOptions['options']); + } else { + $options = array(); + } + + $key = isset($htmlOptions['key']) ? $htmlOptions['key'] : 'primaryKey'; + if (is_array($selection)) { + foreach ($selection as $i => $item) { + if (is_object($item)) { + $selection[$i] = $item->$key; + } + } + } elseif (is_object($selection)) { + $selection = $selection->$key; + } + + foreach ($listData as $key => $value) { + if (is_array($value)) { + $content .= '\n"; + $dummy = array('options' => $options); + if (isset($htmlOptions['encode'])) { + $dummy['encode'] = $htmlOptions['encode']; + } + $content .= static::listOptions($selection, $value, $dummy); + $content .= '' . "\n"; + } else { + $attributes = array('value' => (string)$key, 'encode' => !$raw); + if (!is_array($selection) && !strcmp($key, $selection) || is_array($selection) && in_array($key, $selection)) { + $attributes['selected'] = 'selected'; + } + if (isset($options[$key])) { + $attributes = array_merge($attributes, $options[$key]); + } + $content .= static::tag('option', $attributes, $raw ? (string)$value : static::encode((string)$value)) . "\n"; + } + } + + unset($htmlOptions['key']); + + return $content; + } + + /** + * Renders the HTML tag attributes. + * Since version 1.1.5, attributes whose value is null will not be rendered. + * Special attributes, such as 'checked', 'disabled', 'readonly', will be rendered + * properly based on their corresponding boolean value. + * @param array $attributes attributes to be rendered + * @return string the rendering result + */ + public static function renderAttributes($attributes) + { + static $specialAttributes = 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, + ); + + if ($attributes === array()) { + return ''; + } + + $html = ''; + if (isset($attributes['encode'])) { + $raw = !$attributes['encode']; + unset($attributes['encode']); + } else { + $raw = false; + } + + foreach ($attributes as $name => $value) { + if (isset($specialAttributes[$name])) { + if ($value) { + $html .= ' ' . $name; + if (static::$renderSpecialAttributesValue) { + $html .= '="' . $name . '"'; + } + } + } elseif ($value !== null) { + $html .= ' ' . $name . '="' . ($raw ? $value : static::encode($value)) . '"'; + } + } + return $html; + } } From d2fcc69b34e27b258d8ac3a516106e4f5c359e63 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Fri, 8 Mar 2013 17:32:19 -0500 Subject: [PATCH 60/84] Html WIP --- framework/util/ArrayHelper.php | 32 ++++++ framework/util/Html.php | 215 ++++++++++++++++++----------------------- 2 files changed, 125 insertions(+), 122 deletions(-) diff --git a/framework/util/ArrayHelper.php b/framework/util/ArrayHelper.php index 8bea4b6..fdcd691 100644 --- a/framework/util/ArrayHelper.php +++ b/framework/util/ArrayHelper.php @@ -7,6 +7,7 @@ namespace yii\util; +use Yii; use yii\base\InvalidParamException; /** @@ -279,4 +280,35 @@ class ArrayHelper $args[] = &$array; call_user_func_array('array_multisort', $args); } + + + /** + * Encodes special characters in an array of strings into HTML entities. + * Both the array keys and values will be encoded if needed. + * If a value is an array, this method will also encode it recursively. + * @param array $data data to 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, $charset = null) + { + if ($charset === null) { + $charset = Yii::$app->charset; + } + $d = array(); + foreach ($data as $key => $value) { + if (is_string($key)) { + $key = htmlspecialchars($key, ENT_QUOTES, $charset); + } + if (is_string($value)) { + $value = htmlspecialchars($value, ENT_QUOTES, $charset); + } elseif (is_array($value)) { + $value = static::htmlEncode($value); + } + $d[$key] = $value; + } + return $d; + } } \ No newline at end of file diff --git a/framework/util/Html.php b/framework/util/Html.php index 11b4da6..3a3ee9c 100644 --- a/framework/util/Html.php +++ b/framework/util/Html.php @@ -16,183 +16,154 @@ use Yii; class Html { /** - * @var integer the counter for generating automatic input field names. + * @var boolean whether to close void (empty) elements. Defaults to true. + * @see voidElements */ - public static $count = 0; + public static $closeVoidElements = true; /** - * @var boolean whether to close single tags. Defaults to true. Can be set to false for HTML5. + * @var array list of void elements (element name => 1) + * @see http://www.w3.org/TR/html-markup/syntax.html#void-element */ - public static $closeSingleTags = true; + 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 render special attributes value. Defaults to true. Can be set to false for HTML5. */ public static $renderSpecialAttributesValue = true; + /** * Encodes special characters into HTML entities. - * The {@link CApplication::charset application charset} will be used for encoding. - * @param string $text data to be encoded - * @return string the encoded data + * 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($text) + public static function encode($content) { - return htmlspecialchars($text, ENT_QUOTES, Yii::$app->charset); + return htmlspecialchars($content, ENT_QUOTES, Yii::$app->charset); } /** * Decodes special HTML entities back to the corresponding characters. - * This is the opposite of {@link encode()}. - * @param string $text data to be decoded - * @return string the decoded data + * 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($text) - { - return htmlspecialchars_decode($text, ENT_QUOTES); - } - - /** - * Encodes special characters in an array of strings into HTML entities. - * Both the array keys and values will be encoded if needed. - * If a value is an array, this method will also encode it recursively. - * The {@link CApplication::charset application charset} will be used for encoding. - * @param array $data data to be encoded - * @return array the encoded data - * @see http://www.php.net/manual/en/function.htmlspecialchars.php - */ - public static function encodeArray($data) + public static function decode($content) { - $d = array(); - foreach ($data as $key => $value) { - if (is_string($key)) { - $key = htmlspecialchars($key, ENT_QUOTES, Yii::$app->charset); - } - if (is_string($value)) { - $value = htmlspecialchars($value, ENT_QUOTES, Yii::$app->charset); - } elseif (is_array($value)) { - $value = static::encodeArray($value); - } - $d[$key] = $value; - } - return $d; + return htmlspecialchars_decode($content, ENT_QUOTES); } /** - * Generates an HTML element. - * @param string $tag the tag name - * @param array $htmlOptions the element attributes. The values will be HTML-encoded using {@link encode()}. - * If an 'encode' attribute is given and its value is false, - * the rest of the attribute values will NOT be HTML-encoded. - * Since version 1.1.5, attributes whose value is null will not be rendered. - * @param mixed $content the content to be enclosed between open and close element tags. It will not be HTML-encoded. - * If false, it means there is no body content. - * @param boolean $closeTag whether to generate the close tag. - * @return string the generated HTML element tag + * 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. + * @param array $attributes the element attributes. 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 HTML tag + * @see beginTag + * @see endTag */ - public static function tag($tag, $htmlOptions = array(), $content = false, $closeTag = true) + public static function tag($name, $content = '', $attributes = array()) { - $html = '<' . $tag . static::renderAttributes($htmlOptions); - if ($content === false) { - return $closeTag && static::$closeSingleTags ? $html . ' />' : $html . '>'; + $html = '<' . $name . static::renderAttributes($attributes); + if (isset(static::$voidElements[strtolower($name)])) { + return $html . (static::$closeVoidElements ? ' />' : '>'); } else { - return $closeTag ? $html . '>' . $content . '' : $html . '>' . $content; + return $html . ">$content"; } } /** - * Generates an open HTML element. - * @param string $tag the tag name - * @param array $htmlOptions the element attributes. The values will be HTML-encoded using {@link encode()}. - * If an 'encode' attribute is given and its value is false, - * the rest of the attribute values will NOT be HTML-encoded. - * Since version 1.1.5, attributes whose value is null will not be rendered. - * @return string the generated HTML element tag + * Generates a start tag. + * @param string $name the tag name + * @param array $attributes the element attributes. 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 start tag + * @see endTag + * @see tag */ - public static function openTag($tag, $htmlOptions = array()) + public static function beginTag($name, $attributes = array()) { - return '<' . $tag . static::renderAttributes($htmlOptions) . '>'; + return '<' . $name . static::renderAttributes($attributes) . '>'; } /** - * Generates a close HTML element. - * @param string $tag the tag name - * @return string the generated HTML element tag + * Generates an end tag. + * @param string $name the tag name + * @return string the generated end tag + * @see beginTag + * @see tag */ - public static function closeTag($tag) + public static function endTag($name) { - return ''; + return !static::$closeVoidElements && isset(static::$voidElements[strtolower($name)]) ? '' : ""; } /** - * Encloses the given string within a CDATA tag. - * @param string $text the string to be enclosed + * 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($text) + public static function cdata($content) { - return ''; + return ''; } /** - * Generates a meta tag that can be inserted in the head section of HTML page. - * @param string $content content attribute of the meta tag - * @param string $name name attribute of the meta tag. If null, the attribute will not be generated - * @param string $httpEquiv http-equiv attribute of the meta tag. If null, the attribute will not be generated - * @param array $options other options in name-value pairs (e.g. 'scheme', 'lang') - * @return string the generated meta tag + * Generates a style tag. + * @param string $content the style content + * @param array $attributes the attributes of the style tag. The values will be HTML-encoded using [[encode()]]. + * Attributes whose value is null will be ignored and not put in the tag returned. + * If the attributes does not contain "type", a default one with value "text/css" will be used. + * @return string the generated style tag */ - public static function metaTag($content, $name = null, $httpEquiv = null, $options = array()) + public static function style($content, $attributes = array()) { - if ($name !== null) { - $options['name'] = $name; + if (!isset($attributes['type'])) { + $attributes['type'] = 'text/css'; } - if ($httpEquiv !== null) { - $options['http-equiv'] = $httpEquiv; - } - $options['content'] = $content; - return static::tag('meta', $options); + return static::beginTag('style', $attributes) + . "\n/**/\n" + . static::endTag('style'); } /** - * Generates a link tag that can be inserted in the head section of HTML page. - * Do not confuse this method with {@link link()}. The latter generates a hyperlink. - * @param string $relation rel attribute of the link tag. If null, the attribute will not be generated. - * @param string $type type attribute of the link tag. If null, the attribute will not be generated. - * @param string $href href attribute of the link tag. If null, the attribute will not be generated. - * @param string $media media attribute of the link tag. If null, the attribute will not be generated. - * @param array $options other options in name-value pairs - * @return string the generated link tag - */ - public static function linkTag($relation = null, $type = null, $href = null, $media = null, $options = array()) - { - if ($relation !== null) { - $options['rel'] = $relation; - } - if ($type !== null) { - $options['type'] = $type; - } - if ($href !== null) { - $options['href'] = $href; - } - if ($media !== null) { - $options['media'] = $media; - } - return static::tag('link', $options); - } - - /** - * Encloses the given CSS content with a CSS tag. - * @param string $text the CSS content - * @param string $media the media that this CSS should apply to. - * @return string the CSS properly enclosed + * Generates a script tag. + * @param string $content the script content + * @param array $attributes the attributes of the script tag. The values will be HTML-encoded using [[encode()]]. + * Attributes whose value is null will be ignored and not put in the tag returned. + * If the attributes does not contain "type", a default one with value "text/javascript" will be used. + * @return string the generated script tag */ - public static function css($text, $media = '') + public static function script($content, $attributes = array()) { - if ($media !== '') { - $media = ' media="' . $media . '"'; + if (!isset($attributes['type'])) { + $attributes['type'] = 'text/javascript'; } - return ""; + return static::beginTag('script', $attributes) + . "\n/**/\n" + . static::endTag('script'); } /** From 30d70be071e942b9eb70748b373f0a98f285ff8d Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Sun, 10 Mar 2013 22:00:26 -0400 Subject: [PATCH 61/84] Finished Html helper. --- framework/base/Application.php | 13 +- framework/db/ActiveRelation.php | 16 +- framework/util/ArrayHelper.php | 40 +- framework/util/Html.php | 1308 ++++++++++++++++----------------------- framework/web/Application.php | 47 +- framework/web/Controller.php | 4 + framework/web/Request.php | 39 +- todo.md | 1 + 8 files changed, 663 insertions(+), 805 deletions(-) diff --git a/framework/base/Application.php b/framework/base/Application.php index 3c1a1fb..a60bf90 100644 --- a/framework/base/Application.php +++ b/framework/base/Application.php @@ -382,6 +382,15 @@ class Application extends Module } /** + * Returns the URL manager for this application. + * @return \yii\web\UrlManager the URL manager for this application. + */ + public function getUrlManager() + { + return $this->getComponent('urlManager'); + } + + /** * Returns the internationalization (i18n) component * @return \yii\i18n\I18N the internationalization component */ @@ -411,8 +420,8 @@ class Application extends Module 'i18n' => array( 'class' => 'yii\i18n\I18N', ), - 'securityManager' => array( - 'class' => 'yii\base\SecurityManager', + 'urlManager' => array( + 'class' => 'yii\web\UrlManager', ), )); } diff --git a/framework/db/ActiveRelation.php b/framework/db/ActiveRelation.php index e1793bd..f1b198b 100644 --- a/framework/db/ActiveRelation.php +++ b/framework/db/ActiveRelation.php @@ -55,16 +55,16 @@ class ActiveRelation extends ActiveQuery /** * Specifies the relation associated with the pivot table. * @param string $relationName the relation name. This refers to a relation declared in [[primaryModel]]. - * @param callback $callback a PHP callback for customizing the relation associated with the pivot table. + * @param callable $callable a PHP callback for customizing the relation associated with the pivot table. * Its signature should be `function($query)`, where `$query` is the query to be customized. * @return ActiveRelation the relation object itself. */ - public function via($relationName, $callback = null) + public function via($relationName, $callable = null) { $relation = $this->primaryModel->getRelation($relationName); $this->via = array($relationName, $relation); - if ($callback !== null) { - call_user_func($callback, $relation); + if ($callable !== null) { + call_user_func($callable, $relation); } return $this; } @@ -75,11 +75,11 @@ class ActiveRelation extends ActiveQuery * @param array $link the link between the pivot table and the table associated with [[primaryModel]]. * The keys of the array represent the columns in the pivot table, and the values represent the columns * in the [[primaryModel]] table. - * @param callback $callback a PHP callback for customizing the relation associated with the pivot table. + * @param callable $callable a PHP callback for customizing the relation associated with the pivot table. * Its signature should be `function($query)`, where `$query` is the query to be customized. * @return ActiveRelation */ - public function viaTable($tableName, $link, $callback = null) + public function viaTable($tableName, $link, $callable = null) { $relation = new ActiveRelation(array( 'modelClass' => get_class($this->primaryModel), @@ -89,8 +89,8 @@ class ActiveRelation extends ActiveQuery 'asArray' => true, )); $this->via = $relation; - if ($callback !== null) { - call_user_func($callback, $relation); + if ($callable !== null) { + call_user_func($callable, $relation); } return $this; } diff --git a/framework/util/ArrayHelper.php b/framework/util/ArrayHelper.php index fdcd691..ebf4b23 100644 --- a/framework/util/ArrayHelper.php +++ b/framework/util/ArrayHelper.php @@ -281,33 +281,59 @@ class ArrayHelper 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 needed. + * 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, $charset = null) + public static function htmlEncode($data, $valuesOnly = false, $charset = null) { if ($charset === null) { $charset = Yii::$app->charset; } $d = array(); foreach ($data as $key => $value) { - if (is_string($key)) { + if (!$valuesOnly && is_string($key)) { $key = htmlspecialchars($key, ENT_QUOTES, $charset); } if (is_string($value)) { - $value = htmlspecialchars($value, ENT_QUOTES, $charset); + $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 = false) + { + $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)) { - $value = static::htmlEncode($value); + $d[$key] = static::htmlDecode($value); } - $d[$key] = $value; } return $d; } diff --git a/framework/util/Html.php b/framework/util/Html.php index 3a3ee9c..774a733 100644 --- a/framework/util/Html.php +++ b/framework/util/Html.php @@ -8,8 +8,11 @@ namespace yii\util; 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 */ @@ -22,6 +25,7 @@ class Html 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( @@ -43,9 +47,78 @@ class Html 'wbr' => 1, ); /** - * @var boolean whether to render special attributes value. Defaults to true. Can be set to false for HTML5. - */ - public static $renderSpecialAttributesValue = true; + * @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', + + 'size', + 'maxlength', + 'width', + 'height', + 'rows', + 'cols', + + 'alt', + 'title', + 'rel', + 'media', + ); /** @@ -78,6 +151,7 @@ class Html * 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 $attributes the element attributes. 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 HTML tag @@ -117,7 +191,7 @@ class Html */ public static function endTag($name) { - return !static::$closeVoidElements && isset(static::$voidElements[strtolower($name)]) ? '' : ""; + return ""; } /** @@ -143,9 +217,7 @@ class Html if (!isset($attributes['type'])) { $attributes['type'] = 'text/css'; } - return static::beginTag('style', $attributes) - . "\n/**/\n" - . static::endTag('style'); + return static::tag('style', "\n/**/\n", $attributes); } /** @@ -161,110 +233,77 @@ class Html if (!isset($attributes['type'])) { $attributes['type'] = 'text/javascript'; } - return static::beginTag('script', $attributes) - . "\n/**/\n" - . static::endTag('script'); + return static::tag('script', "\n/**/\n", $attributes); } /** - * Registers a 'refresh' meta tag. - * This method can be invoked anywhere in a view. It will register a 'refresh' - * meta tag with {@link CClientScript} so that the page can be refreshed in - * the specified seconds. - * @param integer $seconds the number of seconds to wait before refreshing the page - * @param string $url the URL to which the page should be redirected to. If empty, it means the current page. - * @since 1.1.1 - */ - public static function refresh($seconds, $url = '') - { - $content = "$seconds"; - if ($url !== '') { - $content .= ';' . static::normalizeUrl($url); - } - Yii::app()->clientScript->registerMetaTag($content, null, 'refresh'); - } - - /** - * Links to the specified CSS file. - * @param string $url the CSS URL - * @param string $media the media that this CSS should apply to. - * @return string the CSS link. + * 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 $attributes the attributes of the link 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 link tag + * @see url */ - public static function cssFile($url, $media = '') + public static function cssFile($url, $attributes = array()) { - return CHtml::linkTag('stylesheet', 'text/css', $url, $media !== '' ? $media : null); + $attributes['rel'] = 'stylesheet'; + $attributes['type'] = 'text/css'; + $attributes['href'] = static::url($url); + return static::tag('link', '', $attributes); } /** - * Encloses the given JavaScript within a script tag. - * @param string $text the JavaScript to be enclosed - * @return string the enclosed JavaScript + * 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 $attributes the attributes of the script 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 script tag + * @see url */ - public static function script($text) + public static function jsFile($url, $attributes = array()) { - return ""; + $attributes['type'] = 'text/javascript'; + $attributes['src'] = static::url($url); + return static::tag('script', '', $attributes); } /** - * Includes a JavaScript file. - * @param string $url URL for the JavaScript file - * @return string the JavaScript file tag + * Generates a form start tag. + * @param array|string $action the form action URL. This parameter will be processed by [[url()]]. + * @param string $method form method, either "post" or "get" (case-insensitive) + * @param array $attributes the attributes of the form 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 form start tag. + * @see endForm */ - public static function scriptFile($url) + public static function beginForm($action = '', $method = 'post', $attributes = array()) { - return ''; - } + $attributes['action'] = $url = static::url($action); + $attributes['method'] = $method; - /** - * Generates an opening form tag. - * This is a shortcut to {@link beginForm}. - * @param mixed $action the form action URL (see {@link normalizeUrl} for details about this parameter.) - * @param string $method form method (e.g. post, get) - * @param array $htmlOptions additional HTML attributes (see {@link tag}). - * @return string the generated form tag. - */ - public static function form($action = '', $method = 'post', $htmlOptions = array()) - { - return static::beginForm($action, $method, $htmlOptions); - } + $form = static::beginTag('form', $attributes); - /** - * Generates an opening form tag. - * Note, only the open tag is generated. A close tag should be placed manually - * at the end of the form. - * @param mixed $action the form action URL (see {@link normalizeUrl} for details about this parameter.) - * @param string $method form method (e.g. post, get) - * @param array $htmlOptions additional HTML attributes (see {@link tag}). - * @return string the generated form tag. - * @see endForm - */ - public static function beginForm($action = '', $method = 'post', $htmlOptions = array()) - { - $htmlOptions['action'] = $url = static::normalizeUrl($action); - $htmlOptions['method'] = $method; - $form = static::tag('form', $htmlOptions, false, false); + // 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($url, '?')) !== false) { foreach (explode('&', substr($url, $pos + 1)) as $pair) { if (($pos = strpos($pair, '=')) !== false) { - $hiddens[] = static::hiddenField(urldecode(substr($pair, 0, $pos)), urldecode(substr($pair, $pos + 1)), array('id' => false)); + $hiddens[] = static::hiddenInput(urldecode(substr($pair, 0, $pos)), urldecode(substr($pair, $pos + 1))); } else { - $hiddens[] = static::hiddenField(urldecode($pair), '', array('id' => false)); + $hiddens[] = static::hiddenInput(urldecode($pair), ''); } } } - $request = Yii::app()->request; - if ($request->enableCsrfValidation && !strcasecmp($method, 'post')) { - $hiddens[] = static::hiddenField($request->csrfTokenName, $request->getCsrfToken(), array('id' => false)); - } if ($hiddens !== array()) { - $form .= "\n" . static::tag('div', array('style' => 'display:none'), implode("\n", $hiddens)); + $form .= "\n" . implode("\n", $hiddens); } + return $form; } /** - * Generates a closing form tag. + * Generates a form end tag. * @return string the generated tag * @see beginForm */ @@ -275,854 +314,599 @@ class Html /** * 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. - * @param mixed $url a URL or an action route that can be used to create a URL. - * See {@link normalizeUrl} for more details about how to specify this parameter. - * @param array $htmlOptions additional HTML attributes. Besides normal HTML attributes, a few special - * attributes are also recognized (see {@link clientChange} and {@link tag} for more details.) + * @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 $attributes the attributes of the hyperlink 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 hyperlink - * @see normalizeUrl - * @see clientChange + * @see url */ - public static function link($text, $url = '#', $htmlOptions = array()) + public static function a($text, $url = null, $attributes = array()) { - if ($url !== '') { - $htmlOptions['href'] = static::normalizeUrl($url); + if ($url !== null) { + $attributes['href'] = static::url($url); } - static::clientChange('click', $htmlOptions); - return static::tag('a', $htmlOptions, $text); + return static::tag('a', $text, $attributes); } /** - * Generates a mailto link. - * @param string $text link body. It will NOT be HTML-encoded. Therefore you can pass in HTML code such as an image tag. - * @param string $email email address. If this is empty, the first parameter (link body) will be treated as the email address. - * @param array $htmlOptions additional HTML attributes. Besides normal HTML attributes, a few special - * attributes are also recognized (see {@link clientChange} and {@link tag} for more details.) + * 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 $attributes the attributes of the hyperlink 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 mailto link - * @see clientChange */ - public static function mailto($text, $email = '', $htmlOptions = array()) + public static function mailto($text, $email = null, $attributes = array()) { - if ($email === '') { - $email = $text; - } - return static::link($text, 'mailto:' . $email, $htmlOptions); + return static::a($text, 'mailto:' . ($email === null ? $text : $email), $attributes); } /** * Generates an image tag. - * @param string $src the image URL - * @param string $alt the alternative text display - * @param array $htmlOptions additional HTML attributes (see {@link tag}). + * @param string $src the image URL. This parameter will be processed by [[url()]]. + * @param array $attributes the attributes of the image 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 image tag */ - public static function image($src, $alt = '', $htmlOptions = array()) + public static function img($src, $attributes = array()) { - $htmlOptions['src'] = $src; - $htmlOptions['alt'] = $alt; - return static::tag('img', $htmlOptions); + $attributes['src'] = static::url($src); + if (!isset($attributes['alt'])) { + $attributes['alt'] = ''; + } + return static::tag('img', null, $attributes); } /** - * Generates a button. - * @param string $label the button label - * @param array $htmlOptions additional HTML attributes. Besides normal HTML attributes, a few special - * attributes are also recognized (see {@link clientChange} and {@link tag} for more details.) - * @return string the generated button tag - * @see clientChange + * 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 $attributes the attributes of the label 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 label tag */ - public static function button($name, $label = 'button', $htmlOptions = array()) + public static function label($content, $for = null, $attributes = array()) { - if (!isset($htmlOptions['name'])) { - if (!array_key_exists('name', $htmlOptions)) { - $htmlOptions['name'] = static::ID_PREFIX . static::$count++; - } - } - if (!isset($htmlOptions['type'])) { - $htmlOptions['type'] = 'button'; - } - if (!isset($htmlOptions['value'])) { - $htmlOptions['value'] = $label; - } - static::clientChange('click', $htmlOptions); - return static::tag('input', $htmlOptions); + $attributes['for'] = $for; + return static::tag('label', $content, $attributes); } /** - * Generates a button using HTML button tag. - * This method is similar to {@link button} except that it generates a 'button' - * tag instead of 'input' tag. - * @param string $label the button label. Note that this value will be directly inserted in the button element - * without being HTML-encoded. - * @param array $htmlOptions additional HTML attributes. Besides normal HTML attributes, a few special - * attributes are also recognized (see {@link clientChange} and {@link tag} for more details.) + * 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 $attributes 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. + * If the attributes does not contain "type", a default one with value "button" will be used. * @return string the generated button tag - * @see clientChange */ - public static function htmlButton($label = 'button', $htmlOptions = array()) + public static function button($name = null, $value = null, $content = 'Button', $attributes = array()) { - if (!isset($htmlOptions['name'])) { - $htmlOptions['name'] = static::ID_PREFIX . static::$count++; - } - if (!isset($htmlOptions['type'])) { - $htmlOptions['type'] = 'button'; + $attributes['name'] = $name; + $attributes['value'] = $value; + if (!isset($attributes['type'])) { + $attributes['type'] = 'button'; } - static::clientChange('click', $htmlOptions); - return static::tag('button', $htmlOptions, $label); + return static::tag('button', $content, $attributes); } /** - * Generates a submit button. - * @param string $label the button label - * @param array $htmlOptions additional HTML attributes. Besides normal HTML attributes, a few special - * attributes are also recognized (see {@link clientChange} and {@link tag} for more details.) - * @return string the generated button tag - * @see clientChange + * 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 $attributes 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 submit button tag */ - public static function submitButton($label = 'submit', $htmlOptions = array()) + public static function submitButton($name = null, $value = null, $content = 'Submit', $attributes = array()) { - $htmlOptions['type'] = 'submit'; - return static::button($label, $htmlOptions); + $attributes['type'] = 'submit'; + return static::button($name, $value, $content, $attributes); } /** - * Generates a reset button. - * @param string $label the button label - * @param array $htmlOptions additional HTML attributes. Besides normal HTML attributes, a few special - * attributes are also recognized (see {@link clientChange} and {@link tag} for more details.) - * @return string the generated button tag - * @see clientChange + * 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 $attributes 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 reset button tag */ - public static function resetButton($label = 'reset', $htmlOptions = array()) + public static function resetButton($name = null, $value = null, $content = 'Reset', $attributes = array()) { - $htmlOptions['type'] = 'reset'; - return static::button($label, $htmlOptions); + $attributes['type'] = 'reset'; + return static::button($name, $value, $content, $attributes); } /** - * Generates an image submit button. - * @param string $src the image URL - * @param array $htmlOptions additional HTML attributes. Besides normal HTML attributes, a few special - * attributes are also recognized (see {@link clientChange} and {@link tag} for more details.) + * 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 $attributes the attributes of the input 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 input tag + */ + public static function input($type, $name = null, $value = null, $attributes = array()) + { + $attributes['type'] = $type; + $attributes['name'] = $name; + $attributes['value'] = $value; + return static::tag('input', null, $attributes); + } + + /** + * 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 $attributes 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 - * @see clientChange */ - public static function imageButton($src, $htmlOptions = array()) + public static function buttonInput($name, $value = 'Button', $attributes = array()) { - $htmlOptions['src'] = $src; - $htmlOptions['type'] = 'image'; - return static::button('submit', $htmlOptions); + return static::input('button', $name, $value, $attributes); } /** - * Generates a link submit button. - * @param string $label the button label - * @param array $htmlOptions additional HTML attributes. Besides normal HTML attributes, a few special - * attributes are also recognized (see {@link clientChange} and {@link tag} for more details.) + * 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 $attributes 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 - * @see clientChange */ - public static function linkButton($label = 'submit', $htmlOptions = array()) + public static function submitInput($name = null, $value = 'Submit', $attributes = array()) { - if (!isset($htmlOptions['submit'])) { - $htmlOptions['submit'] = isset($htmlOptions['href']) ? $htmlOptions['href'] : ''; - } - return static::link($label, '#', $htmlOptions); + return static::input('submit', $name, $value, $attributes); } /** - * Generates a label tag. - * @param string $label label text. Note, you should HTML-encode the text if needed. - * @param string $for the ID of the HTML element that this label is associated with. - * If this is false, the 'for' attribute for the label tag will not be rendered. - * @param array $htmlOptions additional HTML attributes. - * The following HTML option is recognized: - *
    - *
  • required: if this is set and is true, the label will be styled - * with CSS class 'required' (customizable with CHtml::$requiredCss), - * and be decorated with {@link CHtml::beforeRequiredLabel} and - * {@link CHtml::afterRequiredLabel}.
  • - *
- * @return string the generated label tag + * 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 $attributes 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 label($label, $for, $htmlOptions = array()) + public static function resetInput($name = null, $value = 'Reset', $attributes = array()) { - if ($for === false) { - unset($htmlOptions['for']); - } else { - $htmlOptions['for'] = $for; - } - if (isset($htmlOptions['required'])) { - if ($htmlOptions['required']) { - if (isset($htmlOptions['class'])) { - $htmlOptions['class'] .= ' ' . static::$requiredCss; - } else { - $htmlOptions['class'] = static::$requiredCss; - } - $label = static::$beforeRequiredLabel . $label . static::$afterRequiredLabel; - } - unset($htmlOptions['required']); - } - return static::tag('label', $htmlOptions, $label); + return static::input('reset', $name, $value, $attributes); } /** - * Generates a text field input. - * @param string $name the input name - * @param string $value the input value - * @param array $htmlOptions additional HTML attributes. Besides normal HTML attributes, a few special - * attributes are also recognized (see {@link clientChange} and {@link tag} for more details.) - * @return string the generated input field - * @see clientChange - * @see inputField + * 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 $attributes the attributes of the input 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 textField($name, $value = '', $htmlOptions = array()) + public static function textInput($name, $value = null, $attributes = array()) { - static::clientChange('change', $htmlOptions); - return static::inputField('text', $name, $value, $htmlOptions); + return static::input('text', $name, $value, $attributes); } /** - * Generates a hidden input. - * @param string $name the input name - * @param string $value the input value - * @param array $htmlOptions additional HTML attributes (see {@link tag}). - * @return string the generated input field - * @see inputField + * 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 $attributes the attributes of the input 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 hiddenField($name, $value = '', $htmlOptions = array()) + public static function hiddenInput($name, $value = null, $attributes = array()) { - return static::inputField('hidden', $name, $value, $htmlOptions); + return static::input('hidden', $name, $value, $attributes); } /** - * Generates a password field input. - * @param string $name the input name - * @param string $value the input value - * @param array $htmlOptions additional HTML attributes. Besides normal HTML attributes, a few special - * attributes are also recognized (see {@link clientChange} and {@link tag} for more details.) - * @return string the generated input field - * @see clientChange - * @see inputField + * 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 $attributes the attributes of the input 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 passwordField($name, $value = '', $htmlOptions = array()) + public static function passwordInput($name, $value = null, $attributes = array()) { - static::clientChange('change', $htmlOptions); - return static::inputField('password', $name, $value, $htmlOptions); + return static::input('password', $name, $value, $attributes); } /** - * Generates a file input. - * Note, you have to 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 input name - * @param string $value the input value - * @param array $htmlOptions additional HTML attributes (see {@link tag}). - * @return string the generated input field - * @see inputField + * 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 $attributes the attributes of the input 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 fileField($name, $value = '', $htmlOptions = array()) + public static function fileInput($name, $value = null, $attributes = array()) { - return static::inputField('file', $name, $value, $htmlOptions); + return static::input('file', $name, $value, $attributes); } /** * Generates a text area input. * @param string $name the input name - * @param string $value the input value - * @param array $htmlOptions additional HTML attributes. Besides normal HTML attributes, a few special - * attributes are also recognized (see {@link clientChange} and {@link tag} for more details.) - * @return string the generated text area - * @see clientChange - * @see inputField + * @param string $value the input value. Note that it will be encoded using [[encode()]]. + * @param array $attributes the attributes of the input 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 text area tag */ - public static function textArea($name, $value = '', $htmlOptions = array()) + public static function textarea($name, $value = '', $attributes = array()) { - $htmlOptions['name'] = $name; - if (!isset($htmlOptions['id'])) { - $htmlOptions['id'] = static::getIdByName($name); - } elseif ($htmlOptions['id'] === false) { - unset($htmlOptions['id']); - } - static::clientChange('change', $htmlOptions); - return static::tag('textarea', $htmlOptions, isset($htmlOptions['encode']) && !$htmlOptions['encode'] ? $value : static::encode($value)); + $attributes['name'] = $name; + return static::tag('textarea', static::encode($value), $attributes); } /** - * Generates a radio button. - * @param string $name the input name - * @param boolean $checked whether the radio button is checked - * @param array $htmlOptions additional HTML attributes. Besides normal HTML attributes, a few special - * attributes are also recognized (see {@link clientChange} and {@link tag} for more details.) - * Since version 1.1.2, a special option named 'uncheckValue' is available that can be used to specify - * the value returned when the radio button is not checked. When set, a hidden field is rendered so that - * when the radio button is not checked, we can still obtain the posted uncheck value. - * If 'uncheckValue' is not set or set to NULL, the hidden field will not be rendered. - * @return string the generated radio button - * @see clientChange - * @see inputField + * Generates a radio button input. + * @param string $name the name attribute. + * @param string $value the value attribute. If it is null, the value attribute will not be generated. + * @param boolean $checked whether the radio button should be checked. + * @param array $attributes the attributes of the input tag. The values will be HTML-encoded using [[encode()]]. + * Attributes whose value is null will be ignored and not put in the tag returned. The following attribute + * will be specially handled and not put in the resulting tag: + * + * - 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. + * + * @return string the generated radio button tag */ - public static function radioButton($name, $checked = false, $htmlOptions = array()) + public static function radio($name, $value = '1', $checked = false, $attributes = array()) { - if ($checked) { - $htmlOptions['checked'] = 'checked'; - } else { - unset($htmlOptions['checked']); - } - $value = isset($htmlOptions['value']) ? $htmlOptions['value'] : 1; - static::clientChange('click', $htmlOptions); - - if (array_key_exists('uncheckValue', $htmlOptions)) { - $uncheck = $htmlOptions['uncheckValue']; - unset($htmlOptions['uncheckValue']); - } else { - $uncheck = null; - } - - if ($uncheck !== null) { + $attributes['checked'] = $checked; + if (isset($attributes['uncheck'])) { // add a hidden field so that if the radio button is not selected, it still submits a value - if (isset($htmlOptions['id']) && $htmlOptions['id'] !== false) { - $uncheckOptions = array('id' => static::ID_PREFIX . $htmlOptions['id']); - } else { - $uncheckOptions = array('id' => false); - } - $hidden = static::hiddenField($name, $uncheck, $uncheckOptions); + $hidden = static::hiddenInput($name, $attributes['uncheck']); + unset($attributes['uncheck']); } else { $hidden = ''; } - - // add a hidden field so that if the radio button is not selected, it still submits a value - return $hidden . static::inputField('radio', $name, $value, $htmlOptions); + return $hidden . static::input('radio', $name, $value, $attributes); } /** - * Generates a check box. - * @param string $name the input name - * @param boolean $checked whether the check box is checked - * @param array $htmlOptions additional HTML attributes. Besides normal HTML attributes, a few special - * attributes are also recognized (see {@link clientChange} and {@link tag} for more details.) - * Since version 1.1.2, a special option named 'uncheckValue' is available that can be used to specify - * the value returned when the checkbox is not checked. When set, a hidden field is rendered so that - * when the checkbox is not checked, we can still obtain the posted uncheck value. - * If 'uncheckValue' is not set or set to NULL, the hidden field will not be rendered. - * @return string the generated check box - * @see clientChange - * @see inputField + * Generates a checkbox input. + * @param string $name the name attribute. + * @param string $value the value attribute. If it is null, the value attribute will not be generated. + * @param boolean $checked whether the checkbox should be checked. + * @param array $attributes the attributes of the input tag. The values will be HTML-encoded using [[encode()]]. + * Attributes whose value is null will be ignored and not put in the tag returned. The following attribute + * will be specially handled and not put in the resulting tag: + * + * - 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. + * + * @return string the generated checkbox tag */ - public static function checkBox($name, $checked = false, $htmlOptions = array()) + public static function checkbox($name, $value = '1', $checked = false, $attributes = array()) { - if ($checked) { - $htmlOptions['checked'] = 'checked'; - } else { - unset($htmlOptions['checked']); - } - $value = isset($htmlOptions['value']) ? $htmlOptions['value'] : 1; - static::clientChange('click', $htmlOptions); - - if (array_key_exists('uncheckValue', $htmlOptions)) { - $uncheck = $htmlOptions['uncheckValue']; - unset($htmlOptions['uncheckValue']); - } else { - $uncheck = null; - } - - if ($uncheck !== null) { - // add a hidden field so that if the check box is not checked, it still submits a value - if (isset($htmlOptions['id']) && $htmlOptions['id'] !== false) { - $uncheckOptions = array('id' => static::ID_PREFIX . $htmlOptions['id']); - } else { - $uncheckOptions = array('id' => false); - } - $hidden = static::hiddenField($name, $uncheck, $uncheckOptions); + $attributes['checked'] = $checked; + if (isset($attributes['uncheck'])) { + // add a hidden field so that if the checkbox is not selected, it still submits a value + $hidden = static::hiddenInput($name, $attributes['uncheck']); + unset($attributes['uncheck']); } else { $hidden = ''; } - - // add a hidden field so that if the check box is not checked, it still submits a value - return $hidden . static::inputField('checkbox', $name, $value, $htmlOptions); + return $hidden . static::input('checkbox', $name, $value, $attributes); } /** - * Generates a drop down list. + * Generates a drop-down list. * @param string $name the input name - * @param string $select the selected value - * @param array $data data for generating the list options (value=>display). - * You may use {@link listData} to generate this data. - * Please refer to {@link listOptions} on how this data is used to generate the list options. - * Note, the values and labels will be automatically HTML-encoded by this method. - * @param array $htmlOptions additional HTML attributes. Besides normal HTML attributes, a few special - * attributes are recognized. See {@link clientChange} and {@link tag} for more details. - * In addition, the following options are also supported specifically for dropdown list: - *
    - *
  • encode: boolean, specifies whether to encode the values. Defaults to true.
  • - *
  • prompt: string, specifies the prompt text shown as the first list option. Its value is empty. Note, the prompt text will NOT be HTML-encoded.
  • - *
  • empty: string, specifies the text corresponding to empty selection. Its value is empty. - * The 'empty' option can also be an array of value-label pairs. - * Each pair will be used to render a list option at the beginning. Note, the text label will NOT be HTML-encoded.
  • - *
  • options: array, specifies additional attributes for each OPTION tag. - * The array keys must be the option values, and the array values are the extra - * OPTION tag attributes in the name-value pairs. For example, - *
    -	 *     array(
    -	 *         'value1'=>array('disabled'=>true, 'label'=>'value 1'),
    -	 *         'value2'=>array('label'=>'value 2'),
    -	 *     );
    -	 * 
    - *
  • - *
- * Since 1.1.13, a special option named 'unselectValue' is available. It can be used to set the value - * that will be returned when no option is selected in multiple mode. When set, a hidden field is - * rendered so that if no option is selected in multiple mode, we can still obtain the posted - * unselect value. If 'unselectValue' is not set or set to NULL, the hidden field will not be rendered. - * @return string the generated drop down list - * @see clientChange - * @see inputField - * @see listData + * @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\util\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 string $selection the selected value + * @param array $attributes the attributes of the input tag. The values will be HTML-encoded using [[encode()]]. + * Attributes whose value is null will be ignored and not put in the tag returned. The following attribute + * will be specially handled and not put in the resulting tag: + * + * - prompt: string, a prompt text to be displayed as the first option; + * - options: array, the attributes for the 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. + * @return string the generated drop-down list tag */ - public static function dropDownList($name, $select, $data, $htmlOptions = array()) + public static function dropDownList($name, $items, $selection = null, $attributes = array()) { - $htmlOptions['name'] = $name; - - if (!isset($htmlOptions['id'])) { - $htmlOptions['id'] = static::getIdByName($name); - } elseif ($htmlOptions['id'] === false) { - unset($htmlOptions['id']); - } - - static::clientChange('change', $htmlOptions); - $options = "\n" . static::listOptions($select, $data, $htmlOptions); - $hidden = ''; - - if (isset($htmlOptions['multiple'])) { - if (substr($htmlOptions['name'], -2) !== '[]') { - $htmlOptions['name'] .= '[]'; - } - - if (isset($htmlOptions['unselectValue'])) { - $hiddenOptions = isset($htmlOptions['id']) ? array('id' => static::ID_PREFIX . $htmlOptions['id']) : array('id' => false); - $hidden = static::hiddenField(substr($htmlOptions['name'], 0, -2), $htmlOptions['unselectValue'], $hiddenOptions); - unset($htmlOptions['unselectValue']); - } - } - // add a hidden field so that if the option is not selected, it still submits a value - return $hidden . static::tag('select', $htmlOptions, $options); + $attributes['name'] = $name; + $options = static::renderOptions($items, $selection, $attributes); + return static::tag('select', "\n" . $options . "\n", $attributes); } /** * Generates a list box. * @param string $name the input name - * @param mixed $select the selected value(s). This can be either a string for single selection or an array for multiple selections. - * @param array $data data for generating the list options (value=>display) - * You may use {@link listData} to generate this data. - * Please refer to {@link listOptions} on how this data is used to generate the list options. - * Note, the values and labels will be automatically HTML-encoded by this method. - * @param array $htmlOptions additional HTML attributes. Besides normal HTML attributes, a few special - * attributes are also recognized. See {@link clientChange} and {@link tag} for more details. - * In addition, the following options are also supported specifically for list box: - *
    - *
  • encode: boolean, specifies whether to encode the values. Defaults to true.
  • - *
  • prompt: string, specifies the prompt text shown as the first list option. Its value is empty. Note, the prompt text will NOT be HTML-encoded.
  • - *
  • empty: string, specifies the text corresponding to empty selection. Its value is empty. - * The 'empty' option can also be an array of value-label pairs. - * Each pair will be used to render a list option at the beginning. Note, the text label will NOT be HTML-encoded.
  • - *
  • options: array, specifies additional attributes for each OPTION tag. - * The array keys must be the option values, and the array values are the extra - * OPTION tag attributes in the name-value pairs. For example, - *
    -	 *     array(
    -	 *         'value1'=>array('disabled'=>true, 'label'=>'value 1'),
    -	 *         'value2'=>array('label'=>'value 2'),
    -	 *     );
    -	 * 
    - *
  • - *
- * @return string the generated list box - * @see clientChange - * @see inputField - * @see listData - */ - public static function listBox($name, $select, $data, $htmlOptions = array()) - { - if (!isset($htmlOptions['size'])) { - $htmlOptions['size'] = 4; + * @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\util\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 string|array $selection the selected value(s) + * @param array $attributes the attributes of the input tag. The values will be HTML-encoded using [[encode()]]. + * Attributes whose value is null will be ignored and not put in the tag returned. The following attribute + * will be specially handled and not put in the resulting tag: + * + * - prompt: string, a prompt text to be displayed as the first option; + * - options: array, the attributes for the 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. + * @return string the generated list box tag + */ + public static function listBox($name, $items, $selection = null, $attributes = array()) + { + if (!isset($attributes['size'])) { + $attributes['size'] = 4; + } + if (isset($attributes['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, $attributes['unselect']); + unset($attributes['unselect']); + } else { + $hidden = ''; } - if (isset($htmlOptions['multiple'])) { - if (substr($name, -2) !== '[]') { - $name .= '[]'; - } + if (isset($attributes['multiple']) && $attributes['multiple'] && substr($name, -2) !== '[]') { + $name .= '[]'; } - return static::dropDownList($name, $select, $data, $htmlOptions); + $attributes['name'] = $name; + $options = static::renderOptions($items, $selection, $attributes); + return $hidden . static::tag('select', "\n" . $options . "\n", $attributes); } /** - * Generates a check box list. - * A check box list allows multiple selection, like {@link listBox}. - * As a result, the corresponding POST value is an array. - * @param string $name name of the check box list. You can use this name to retrieve - * the selected value(s) once the form is submitted. - * @param mixed $select selection of the check boxes. This can be either a string - * for single selection or an array for multiple selections. - * @param array $data value-label pairs used to generate the check box list. - * Note, the values will be automatically HTML-encoded, while the labels will not. - * @param array $htmlOptions additional HTML options. The options will be applied to - * each checkbox input. The following special options are recognized: - *
    - *
  • template: string, specifies how each checkbox is rendered. Defaults - * to "{input} {label}", where "{input}" will be replaced by the generated - * check box input tag while "{label}" be replaced by the corresponding check box label.
  • - *
  • separator: string, specifies the string that separates the generated check boxes.
  • - *
  • checkAll: string, specifies the label for the "check all" checkbox. - * If this option is specified, a 'check all' checkbox will be displayed. Clicking on - * this checkbox will cause all checkboxes checked or unchecked.
  • - *
  • checkAllLast: boolean, specifies whether the 'check all' checkbox should be - * displayed at the end of the checkbox list. If this option is not set (default) - * or is false, the 'check all' checkbox will be displayed at the beginning of - * the checkbox list.
  • - *
  • labelOptions: array, specifies the additional HTML attributes to be rendered - * for every label tag in the list.
  • - *
  • container: string, specifies the checkboxes enclosing tag. Defaults to 'span'. - * If the value is an empty string, no enclosing tag will be generated
  • - *
  • baseID: string, specifies the base ID prefix to be used for checkboxes in the list. - * This option is available since version 1.1.13.
  • - *
- * @return string the generated check box list + * 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 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 string|array $selection the selected value(s). + * @param callable $formatter a callback that can be used to customize the generation of the HTML code + * corresponding to a single checkbox. The signature of this callback must be: + * + * ~~~ + * function ($index, $label, $name, $value, $checked) + * ~~~ + * + * 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, $select, $data, $htmlOptions = array()) + public static function checkboxList($name, $items, $selection = null, $formatter = null) { - $template = isset($htmlOptions['template']) ? $htmlOptions['template'] : '{input} {label}'; - $separator = isset($htmlOptions['separator']) ? $htmlOptions['separator'] : "
\n"; - $container = isset($htmlOptions['container']) ? $htmlOptions['container'] : 'span'; - unset($htmlOptions['template'], $htmlOptions['separator'], $htmlOptions['container']); - if (substr($name, -2) !== '[]') { $name .= '[]'; } - if (isset($htmlOptions['checkAll'])) { - $checkAllLabel = $htmlOptions['checkAll']; - $checkAllLast = isset($htmlOptions['checkAllLast']) && $htmlOptions['checkAllLast']; - } - unset($htmlOptions['checkAll'], $htmlOptions['checkAllLast']); - - $labelOptions = isset($htmlOptions['labelOptions']) ? $htmlOptions['labelOptions'] : array(); - unset($htmlOptions['labelOptions']); - - $items = array(); - $baseID = isset($htmlOptions['baseID']) ? $htmlOptions['baseID'] : static::getIdByName($name); - unset($htmlOptions['baseID']); - $id = 0; - $checkAll = true; - - foreach ($data as $value => $label) { - $checked = !is_array($select) && !strcmp($value, $select) || is_array($select) && in_array($value, $select); - $checkAll = $checkAll && $checked; - $htmlOptions['value'] = $value; - $htmlOptions['id'] = $baseID . '_' . $id++; - $option = static::checkBox($name, $checked, $htmlOptions); - $label = static::label($label, $htmlOptions['id'], $labelOptions); - $items[] = strtr($template, array('{input}' => $option, '{label}' => $label)); - } - - if (isset($checkAllLabel)) { - $htmlOptions['value'] = 1; - $htmlOptions['id'] = $id = $baseID . '_all'; - $option = static::checkBox($id, $checkAll, $htmlOptions); - $label = static::label($checkAllLabel, $id, $labelOptions); - $item = strtr($template, array('{input}' => $option, '{label}' => $label)); - if ($checkAllLast) { - $items[] = $item; + $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, $value, $checked); } else { - array_unshift($items, $item); + $lines[] = static::label(static::checkbox($name, $value, $checked) . ' ' . $label); } - $name = strtr($name, array('[' => '\\[', ']' => '\\]')); - $js = <<getClientScript(); - $cs->registerCoreScript('jquery'); - $cs->registerScript($id, $js); + $index++; } - if (empty($container)) { - return implode($separator, $items); - } else { - return static::tag($container, array('id' => $baseID), implode($separator, $items)); - } + return implode("\n", $lines); } /** - * Generates a radio button list. - * A radio button list is like a {@link checkBoxList check box list}, except that - * it only allows single selection. - * @param string $name name of the radio button list. You can use this name to retrieve - * the selected value(s) once the form is submitted. - * @param string $select selection of the radio buttons. - * @param array $data value-label pairs used to generate the radio button list. - * Note, the values will be automatically HTML-encoded, while the labels will not. - * @param array $htmlOptions additional HTML options. The options will be applied to - * each radio button input. The following special options are recognized: - *
    - *
  • template: string, specifies how each radio button is rendered. Defaults - * to "{input} {label}", where "{input}" will be replaced by the generated - * radio button input tag while "{label}" will be replaced by the corresponding radio button label.
  • - *
  • separator: string, specifies the string that separates the generated radio buttons. Defaults to new line (
    ).
  • - *
  • labelOptions: array, specifies the additional HTML attributes to be rendered - * for every label tag in the list.
  • - *
  • container: string, specifies the radio buttons enclosing tag. Defaults to 'span'. - * If the value is an empty string, no enclosing tag will be generated
  • - *
  • baseID: string, specifies the base ID prefix to be used for radio buttons in the list. - * This option is available since version 1.1.13.
  • - *
- * @return string the generated radio button list - */ - public static function radioButtonList($name, $select, $data, $htmlOptions = array()) - { - $template = isset($htmlOptions['template']) ? $htmlOptions['template'] : '{input} {label}'; - $separator = isset($htmlOptions['separator']) ? $htmlOptions['separator'] : "
\n"; - $container = isset($htmlOptions['container']) ? $htmlOptions['container'] : 'span'; - unset($htmlOptions['template'], $htmlOptions['separator'], $htmlOptions['container']); - - $labelOptions = isset($htmlOptions['labelOptions']) ? $htmlOptions['labelOptions'] : array(); - unset($htmlOptions['labelOptions']); - - $items = array(); - $baseID = isset($htmlOptions['baseID']) ? $htmlOptions['baseID'] : static::getIdByName($name); - unset($htmlOptions['baseID']); - $id = 0; - foreach ($data as $value => $label) { - $checked = !strcmp($value, $select); - $htmlOptions['value'] = $value; - $htmlOptions['id'] = $baseID . '_' . $id++; - $option = static::radioButton($name, $checked, $htmlOptions); - $label = static::label($label, $htmlOptions['id'], $labelOptions); - $items[] = strtr($template, array('{input}' => $option, '{label}' => $label)); - } - if (empty($container)) { - return implode($separator, $items); - } else { - return static::tag($container, array('id' => $baseID), implode($separator, $items)); - } - } - - /** - * Normalizes the input parameter to be a valid URL. - * - * If the input parameter is an empty string, the currently requested URL will be returned. - * - * If the input parameter is a non-empty string, it is treated as a valid URL and will - * be returned without any change. + * 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 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 string|array $selection the selected value(s). + * @param callable $formatter a callback that can be used to customize the generation of the HTML code + * corresponding to a single radio button. The signature of this callback must be: * - * If the input parameter is an array, it is treated as a controller route and a list of - * GET parameters, and the {@link CController::createUrl} method will be invoked to - * create a URL. In this case, the first array element refers to the controller route, - * and the rest key-value pairs refer to the additional GET parameters for the URL. - * For example, array('post/list', 'page'=>3) may be used to generate the URL - * /index.php?r=post/list&page=3. + * ~~~ + * function ($index, $label, $name, $value, $checked) + * ~~~ * - * @param mixed $url the parameter to be used to generate a valid URL - * @return string the normalized URL + * 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 normalizeUrl($url) + public static function radioList($name, $items, $selection = null, $formatter = null) { - if (is_array($url)) { - if (isset($url[0])) { - if (($c = Yii::app()->getController()) !== null) { - $url = $c->createUrl($url[0], array_splice($url, 1)); - } else { - $url = Yii::app()->createUrl($url[0], array_splice($url, 1)); - } + $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, $value, $checked); } else { - $url = ''; + $lines[] = static::label(static::radio($name, $value, $checked) . ' ' . $label); } + $index++; } - return $url === '' ? Yii::app()->getRequest()->getUrl() : $url; - } - /** - * Generates an input HTML tag. - * This method generates an input HTML tag based on the given input name and value. - * @param string $type the input type (e.g. 'text', 'radio') - * @param string $name the input name - * @param string $value the input value - * @param array $htmlOptions additional HTML attributes for the HTML tag (see {@link tag}). - * @return string the generated input tag - */ - protected static function inputField($type, $name, $value, $htmlOptions) - { - $htmlOptions['type'] = $type; - $htmlOptions['value'] = $value; - $htmlOptions['name'] = $name; - if (!isset($htmlOptions['id'])) { - $htmlOptions['id'] = static::getIdByName($name); - } elseif ($htmlOptions['id'] === false) { - unset($htmlOptions['id']); - } - return static::tag('input', $htmlOptions); + return implode("\n", $lines); } /** - * Generates the list options. - * @param mixed $selection the selected value(s). This can be either a string for single selection or an array for multiple selections. - * @param array $listData the option data (see {@link listData}) - * @param array $htmlOptions additional HTML attributes. The following two special attributes are recognized: - *
    - *
  • encode: boolean, specifies whether to encode the values. Defaults to true.
  • - *
  • prompt: string, specifies the prompt text shown as the first list option. Its value is empty. Note, the prompt text will NOT be HTML-encoded.
  • - *
  • empty: string, specifies the text corresponding to empty selection. Its value is empty. - * The 'empty' option can also be an array of value-label pairs. - * Each pair will be used to render a list option at the beginning. Note, the text label will NOT be HTML-encoded.
  • - *
  • options: array, specifies additional attributes for each OPTION tag. - * The array keys must be the option values, and the array values are the extra - * OPTION tag attributes in the name-value pairs. For example, - *
    -	 *     array(
    -	 *         'value1'=>array('disabled'=>true, 'label'=>'value 1'),
    -	 *         'value2'=>array('label'=>'value 2'),
    -	 *     );
    -	 * 
    - *
  • - *
  • key: string, specifies the name of key attribute of the selection object(s). - * This is used when the selection is represented in terms of objects. In this case, - * the property named by the key option of the objects will be treated as the actual selection value. - * This option defaults to 'primaryKey', meaning using the 'primaryKey' property value of the objects in the selection. - * This option has been available since version 1.1.3.
  • - *
+ * Renders the option tags that can be used by [[dropDownList()]] and [[listBox()]]. + * @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\util\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 string|array $selection the selected value(s). This can be either a string for single selection + * or an array for multiple selections. + * @param array $attributes the attributes 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 listOptions($selection, $listData, &$htmlOptions) + public static function renderOptions($items, $selection = null, &$attributes = array()) { - $raw = isset($htmlOptions['encode']) && !$htmlOptions['encode']; - $content = ''; - if (isset($htmlOptions['prompt'])) { - $content .= '\n"; - unset($htmlOptions['prompt']); - } - if (isset($htmlOptions['empty'])) { - if (!is_array($htmlOptions['empty'])) { - $htmlOptions['empty'] = array('' => $htmlOptions['empty']); - } - foreach ($htmlOptions['empty'] as $value => $label) { - $content .= '\n"; - } - unset($htmlOptions['empty']); + $lines = array(); + if (isset($attributes['prompt'])) { + $prompt = strtr(static::encode($attributes['prompt']), ' ', ' '); + $lines[] = static::tag('option', $prompt, array('value' => '')); } - if (isset($htmlOptions['options'])) { - $options = $htmlOptions['options']; - unset($htmlOptions['options']); - } else { - $options = array(); - } + $options = isset($attributes['options']) ? $attributes['options'] : array(); + $groups = isset($attributes['groups']) ? $attributes['groups'] : array(); + unset($attributes['prompt'], $attributes['options'], $attributes['groups']); - $key = isset($htmlOptions['key']) ? $htmlOptions['key'] : 'primaryKey'; - if (is_array($selection)) { - foreach ($selection as $i => $item) { - if (is_object($item)) { - $selection[$i] = $item->$key; - } - } - } elseif (is_object($selection)) { - $selection = $selection->$key; - } - - foreach ($listData as $key => $value) { + foreach ($items as $key => $value) { if (is_array($value)) { - $content .= '\n"; - $dummy = array('options' => $options); - if (isset($htmlOptions['encode'])) { - $dummy['encode'] = $htmlOptions['encode']; - } - $content .= static::listOptions($selection, $value, $dummy); - $content .= '' . "\n"; + $groupAttrs = isset($groups[$key]) ? $groups[$key] : array(); + $groupAttrs['label'] = $key; + $attrs = array('options' => $options, 'groups' => $groups); + $content = static::renderOptions($selection, $value, $attrs); + $lines[] = static::tag('optgroup', "\n" . $content . "\n", $groupAttrs); } else { - $attributes = array('value' => (string)$key, 'encode' => !$raw); - if (!is_array($selection) && !strcmp($key, $selection) || is_array($selection) && in_array($key, $selection)) { - $attributes['selected'] = 'selected'; - } - if (isset($options[$key])) { - $attributes = array_merge($attributes, $options[$key]); - } - $content .= static::tag('option', $attributes, $raw ? (string)$value : static::encode((string)$value)) . "\n"; + $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', strtr(static::encode($value), ' ', ' '), $attrs); } } - unset($htmlOptions['key']); - - return $content; + return implode("\n", $lines); } /** * Renders the HTML tag attributes. - * Since version 1.1.5, attributes whose value is null will not be rendered. - * Special attributes, such as 'checked', 'disabled', 'readonly', will be rendered - * properly based on their corresponding boolean value. - * @param array $attributes attributes to be rendered - * @return string the rendering result + * 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 renderAttributes($attributes) { - static $specialAttributes = 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, - ); - - if ($attributes === array()) { - return ''; + 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 = ''; - if (isset($attributes['encode'])) { - $raw = !$attributes['encode']; - unset($attributes['encode']); - } else { - $raw = false; - } - foreach ($attributes as $name => $value) { - if (isset($specialAttributes[$name])) { - if ($value) { - $html .= ' ' . $name; - if (static::$renderSpecialAttributesValue) { - $html .= '="' . $name . '"'; - } + if (isset(static::$booleanAttributes[strtolower($name)])) { + if ($value || strcasecmp($name, $value) === 0) { + $html .= static::$showBooleanAttributeValues ? " $name=\"$name\"" : " $name"; } } elseif ($value !== null) { - $html .= ' ' . $name . '="' . ($raw ? $value : static::encode($value)) . '"'; + $html .= " $name=\"" . static::encode($value) . '"'; } } - return $html; } + + /** + * Normalizes the input parameter to be a valid URL. + * + * If the input parameter + * + * - is an empty string: the currently requested URL will be returned; + * - is a non-empty string: it will be processed by [[Yii::getAlias()]] which, if the string is an alias, + * will be resolved into a URL; + * - is an array: the first array element is considered a route, while the rest of the name-value + * pairs are considered as the parameters to be used for URL creation using [[\yii\base\Application::createUrl()]]. + * Here are some examples: `array('post/index', 'page' => 2)`, `array('index')`. + * + * @param array|string $url the parameter to be used to generate a valid URL + * @return string the normalized URL + * @throws InvalidParamException if the parameter is invalid. + */ + public static function url($url) + { + if (is_array($url)) { + if (isset($url[0])) { + return Yii::$app->createUrl($url[0], array_splice($url, 1)); + } else { + throw new InvalidParamException('The array specifying a URL must contain at least one element.'); + } + } elseif ($url === '') { + return Yii::$app->getRequest()->getUrl(); + } else { + return Yii::getAlias($url); + } + } } diff --git a/framework/web/Application.php b/framework/web/Application.php index d2a758a..0b9ce06 100644 --- a/framework/web/Application.php +++ b/framework/web/Application.php @@ -6,6 +6,7 @@ */ namespace yii\web; +use yii\base\InvalidParamException; /** * Application is the base class for all application classes. @@ -62,11 +63,48 @@ class Application extends \yii\base\Application } /** - * @return UrlManager + * Creates a URL using the given route and parameters. + * + * This method first normalizes the given route by converting a relative route into an absolute one. + * A relative route is a route without slash. If the route is an empty string, it stands for + * the route of the currently active [[controller]]. If the route is not empty, it stands for + * an action ID of the [[controller]]. + * + * After normalizing the route, this method calls [[\yii\web\UrlManager::createUrl()]] + * to create a relative URL. + * + * @param string $route the route. This can be either an absolute or a relative route. + * @param array $params the parameters (name-value pairs) to be included in the generated URL + * @return string the created URL + * @throws InvalidParamException if a relative route is given and there is no active controller. + * @see createAbsoluteUrl */ - public function getUrlManager() + public function createUrl($route, $params = array()) { - return $this->getComponent('urlManager'); + if (strpos($route, '/') === false) { + // a relative route + if ($this->controller !== null) { + $route = $route === '' ? $this->controller->route : $this->controller->uniqueId . '/' . $route; + } else { + throw new InvalidParamException('No active controller exists for resolving a relative route.'); + } + } + return $this->getUrlManager()->createUrl($route, $params); + } + + /** + * Creates an absolute URL using the given route and parameters. + * This method first calls [[createUrl()]] to create a relative URL. + * It then prepends [[\yii\web\UrlManager::hostInfo]] to the URL to form an absolute one. + * @param string $route the route. This can be either an absolute or a relative route. + * See [[createUrl()]] for more details. + * @param array $params the parameters (name-value pairs) + * @return string the created URL + * @see createUrl + */ + public function createAbsoluteUrl($route, $params = array()) + { + return $this->getUrlManager()->getHostInfo() . $this->createUrl($route, $params); } /** @@ -86,9 +124,6 @@ class Application extends \yii\base\Application 'session' => array( 'class' => 'yii\web\Session', ), - 'urlManager' => array( - 'class' => 'yii\web\UrlManager', - ), )); } } diff --git a/framework/web/Controller.php b/framework/web/Controller.php index 2779c35..9420034 100644 --- a/framework/web/Controller.php +++ b/framework/web/Controller.php @@ -16,4 +16,8 @@ namespace yii\web; */ class Controller extends \yii\base\Controller { + public function createUrl($route, $params = array()) + { + + } } \ No newline at end of file diff --git a/framework/web/Request.php b/framework/web/Request.php index 65a7f30..c7899cf 100644 --- a/framework/web/Request.php +++ b/framework/web/Request.php @@ -368,7 +368,7 @@ class Request extends \yii\base\Request */ protected function resolvePathInfo() { - $pathInfo = $this->getRequestUri(); + $pathInfo = $this->getUrl(); if (($pos = strpos($pathInfo, '?')) !== false) { $pathInfo = substr($pathInfo, 0, $pos); @@ -407,42 +407,41 @@ class Request extends \yii\base\Request } /** - * Returns the currently requested URL. - * This is a shortcut to the concatenation of [[hostInfo]] and [[requestUri]]. - * @return string the currently requested URL. + * Returns the currently requested absolute URL. + * This is a shortcut to the concatenation of [[hostInfo]] and [[url]]. + * @return string the currently requested absolute URL. */ - public function getUrl() + public function getAbsoluteUrl() { - return $this->getHostInfo() . $this->getRequestUri(); + return $this->getHostInfo() . $this->getUrl(); } - private $_requestUri; + private $_url; /** - * Returns the portion after [[hostInfo]] for the currently requested URL. - * This refers to the portion that is after the [[hostInfo]] part. It includes the [[queryString]] part if any. - * The implementation of this method referenced Zend_Controller_Request_Http in Zend Framework. - * @return string the request URI portion for the currently requested URL. - * Note that the URI returned is URL-encoded. - * @throws InvalidConfigException if the request URI cannot be determined due to unusual server configuration + * Returns the currently requested relative URL. + * This refers to the portion of the URL that is after the [[hostInfo]] part. + * It includes the [[queryString]] part if any. + * @return string the currently requested relative URL. Note that the URI returned is URL-encoded. + * @throws InvalidConfigException if the URL cannot be determined due to unusual server configuration */ - public function getRequestUri() + public function getUrl() { - if ($this->_requestUri === null) { - $this->_requestUri = $this->resolveRequestUri(); + if ($this->_url === null) { + $this->_url = $this->resolveRequestUri(); } - return $this->_requestUri; + return $this->_url; } /** - * Sets the currently requested URI. + * Sets the currently requested relative URL. * The URI must refer to the portion that is after [[hostInfo]]. * Note that the URI should be URL-encoded. * @param string $value the request URI to be set */ - public function setRequestUri($value) + public function setUrl($value) { - $this->_requestUri = $value; + $this->_url = $value; } /** diff --git a/todo.md b/todo.md index c80da5a..03b9f70 100644 --- a/todo.md +++ b/todo.md @@ -18,6 +18,7 @@ * backend-specific unit tests * dependency unit tests - validators + * Refactor validators to add validateValue() for every validator, if possible. Check if value is an array. * FileValidator: depends on CUploadedFile * CaptchaValidator: depends on CaptchaAction * DateValidator: should we use CDateTimeParser, or simply use strtotime()? From 03e212bcf671e28ecc36445017f5d214bcf5405d Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Mon, 11 Mar 2013 11:41:46 -0400 Subject: [PATCH 62/84] Added HtmlTest. --- framework/util/Html.php | 78 ++++++--- framework/web/Application.php | 10 +- tests/unit/framework/util/ArrayHelperTest.php | 2 +- tests/unit/framework/util/HtmlTest.php | 234 ++++++++++++++++++++++++++ tests/unit/runtime/.gitignore | 1 + 5 files changed, 297 insertions(+), 28 deletions(-) create mode 100644 tests/unit/framework/util/HtmlTest.php create mode 100644 tests/unit/runtime/.gitignore diff --git a/framework/util/Html.php b/framework/util/Html.php index 774a733..8c29047 100644 --- a/framework/util/Html.php +++ b/framework/util/Html.php @@ -217,7 +217,7 @@ class Html if (!isset($attributes['type'])) { $attributes['type'] = 'text/css'; } - return static::tag('style', "\n/**/\n", $attributes); + return static::tag('style', "/**/", $attributes); } /** @@ -233,7 +233,7 @@ class Html if (!isset($attributes['type'])) { $attributes['type'] = 'text/javascript'; } - return static::tag('script', "\n/**/\n", $attributes); + return static::tag('script', "/**/", $attributes); } /** @@ -278,23 +278,25 @@ class Html */ public static function beginForm($action = '', $method = 'post', $attributes = array()) { - $attributes['action'] = $url = static::url($action); - $attributes['method'] = $method; - - $form = static::beginTag('form', $attributes); + $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($url, '?')) !== false) { - foreach (explode('&', substr($url, $pos + 1)) as $pair) { - if (($pos = strpos($pair, '=')) !== false) { - $hiddens[] = static::hiddenInput(urldecode(substr($pair, 0, $pos)), urldecode(substr($pair, $pos + 1))); + 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); } + + $attributes['action'] = $action; + $attributes['method'] = $method; + $form = static::beginTag('form', $attributes); if ($hiddens !== array()) { $form .= "\n" . implode("\n", $hiddens); } @@ -696,17 +698,20 @@ class Html if (!isset($attributes['size'])) { $attributes['size'] = 4; } + if (isset($attributes['multiple']) && $attributes['multiple'] && substr($name, -2) !== '[]') { + $name .= '[]'; + } + $attributes['name'] = $name; if (isset($attributes['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, $attributes['unselect']); unset($attributes['unselect']); } else { $hidden = ''; } - if (isset($attributes['multiple']) && $attributes['multiple'] && substr($name, -2) !== '[]') { - $name .= '[]'; - } - $attributes['name'] = $name; $options = static::renderOptions($items, $selection, $attributes); return $hidden . static::tag('select', "\n" . $options . "\n", $attributes); } @@ -720,8 +725,13 @@ class Html * 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 string|array $selection the selected value(s). - * @param callable $formatter a callback that can be used to customize the generation of the HTML code - * corresponding to a single checkbox. The signature of this callback must be: + * @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, $value, $checked) @@ -732,12 +742,13 @@ class Html * value and the checked status of the checkbox input. * @return string the generated checkbox list */ - public static function checkboxList($name, $items, $selection = null, $formatter = null) + public static function checkboxList($name, $items, $selection = null, $options = array()) { if (substr($name, -2) !== '[]') { $name .= '[]'; } + $formatter = isset($options['item']) ? $options['item'] : null; $lines = array(); $index = 0; foreach ($items as $value => $label) { @@ -752,7 +763,16 @@ class Html $index++; } - return implode("\n", $lines); + 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); } /** @@ -763,8 +783,13 @@ class Html * 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 string|array $selection the selected value(s). - * @param callable $formatter a callback that can be used to customize the generation of the HTML code - * corresponding to a single radio button. The signature of this callback must be: + * @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, $value, $checked) @@ -775,8 +800,9 @@ class Html * value and the checked status of the radio button input. * @return string the generated radio button list */ - public static function radioList($name, $items, $selection = null, $formatter = null) + public static function radioList($name, $items, $selection = null, $options = array()) { + $formatter = isset($options['item']) ? $options['item'] : null; $lines = array(); $index = 0; foreach ($items as $value => $label) { @@ -791,7 +817,15 @@ class Html $index++; } - return implode("\n", $lines); + $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); } /** diff --git a/framework/web/Application.php b/framework/web/Application.php index 0b9ce06..61e84a3 100644 --- a/framework/web/Application.php +++ b/framework/web/Application.php @@ -66,9 +66,9 @@ class Application extends \yii\base\Application * Creates a URL using the given route and parameters. * * This method first normalizes the given route by converting a relative route into an absolute one. - * A relative route is a route without slash. If the route is an empty string, it stands for - * the route of the currently active [[controller]]. If the route is not empty, it stands for - * an action ID of the [[controller]]. + * A relative route is a route without a leading slash. It is considered to be relative to the currently + * requested route. If the route is an empty string, it stands for the route of the currently active + * [[controller]]. Otherwise, the [[Controller::uniqueId]] will be prepended to the route. * * After normalizing the route, this method calls [[\yii\web\UrlManager::createUrl()]] * to create a relative URL. @@ -81,12 +81,12 @@ class Application extends \yii\base\Application */ public function createUrl($route, $params = array()) { - if (strpos($route, '/') === false) { + if (strncmp($route, '/', 1) !== 0) { // a relative route if ($this->controller !== null) { $route = $route === '' ? $this->controller->route : $this->controller->uniqueId . '/' . $route; } else { - throw new InvalidParamException('No active controller exists for resolving a relative route.'); + throw new InvalidParamException('Relative route cannot be handled because there is no active controller.'); } } return $this->getUrlManager()->createUrl($route, $params); diff --git a/tests/unit/framework/util/ArrayHelperTest.php b/tests/unit/framework/util/ArrayHelperTest.php index a713381..a36ce68 100644 --- a/tests/unit/framework/util/ArrayHelperTest.php +++ b/tests/unit/framework/util/ArrayHelperTest.php @@ -1,6 +1,6 @@ array( + 'request' => array( + 'class' => 'yii\web\Request', + 'url' => '/test', + ), + ), + )); + } + + public function tearDown() + { + Yii::$app = null; + } + + public function testEncode() + { + $this->assertEquals("a<>&"'", Html::encode("a<>&\"'")); + } + + public function testDecode() + { + $this->assertEquals("a<>&\"'", Html::decode("a<>&"'")); + } + + public function testTag() + { + $this->assertEquals('
', Html::tag('br')); + $this->assertEquals('', Html::tag('span')); + $this->assertEquals('
content
', Html::tag('div', 'content')); + $this->assertEquals('', Html::tag('input', '', array('type' => 'text', 'name' => 'test', 'value' => '<>'))); + + Html::$closeVoidElements = false; + + $this->assertEquals('
', Html::tag('br')); + $this->assertEquals('', Html::tag('span')); + $this->assertEquals('
content
', Html::tag('div', 'content')); + $this->assertEquals('', Html::tag('input', '', array('type' => 'text', 'name' => 'test', 'value' => '<>'))); + + Html::$closeVoidElements = true; + } + + public function testBeginTag() + { + $this->assertEquals('
', Html::beginTag('br')); + $this->assertEquals('', Html::beginTag('span', array('id' => 'test', 'class' => 'title'))); + } + + public function testEndTag() + { + $this->assertEquals('
', Html::endTag('br')); + $this->assertEquals('
', Html::endTag('span')); + } + + public function testCdata() + { + $data = 'test<>'; + $this->assertEquals('', Html::cdata($data)); + } + + public function testStyle() + { + $content = 'a <>'; + $this->assertEquals("", Html::style($content)); + $this->assertEquals("", Html::style($content, array('type' => 'text/less'))); + } + + public function testScript() + { + $content = 'a <>'; + $this->assertEquals("", Html::script($content)); + $this->assertEquals("", Html::script($content, array('type' => 'text/js'))); + } + + public function testCssFile() + { + $this->assertEquals('', Html::cssFile('http://example.com')); + $this->assertEquals('', Html::cssFile('')); + } + + public function testJsFile() + { + $this->assertEquals('', Html::jsFile('http://example.com')); + $this->assertEquals('', Html::jsFile('')); + } + + public function testBeginForm() + { + $this->assertEquals('
', Html::beginForm()); + $this->assertEquals('', Html::beginForm('/example', 'get')); + $hiddens = array( + '', + '', + ); + $this->assertEquals('' . "\n" . implode("\n", $hiddens), Html::beginForm('/example?id=1&title=%3C', 'get')); + } + + public function testEndForm() + { + $this->assertEquals('
', Html::endForm()); + } + + public function testA() + { + $this->assertEquals('something<>', Html::a('something<>')); + $this->assertEquals('something', Html::a('something', '/example')); + $this->assertEquals('something', Html::a('something', '')); + } + + public function testMailto() + { + $this->assertEquals('test<>', Html::mailto('test<>')); + $this->assertEquals('test<>', Html::mailto('test<>', 'test>')); + } + + public function testImg() + { + $this->assertEquals('', Html::img('/example')); + $this->assertEquals('', Html::img('')); + $this->assertEquals('something', Html::img('/example', array('alt' => 'something', 'width' => 10))); + } + + public function testLabel() + { + $this->assertEquals('', Html::label('something<>')); + $this->assertEquals('', Html::label('something<>', 'a')); + $this->assertEquals('', Html::label('something<>', 'a', array('class' => 'test'))); + } + + public function testButton() + { + $this->assertEquals('', Html::button()); + $this->assertEquals('', Html::button('test', 'value', 'content<>')); + $this->assertEquals('', Html::button('test', 'value', 'content<>', array('type' => 'submit', 'class' => "t"))); + } + + public function testSubmitButton() + { + $this->assertEquals('', Html::submitButton()); + $this->assertEquals('', Html::submitButton('test', 'value', 'content<>', array('class' => 't'))); + } + + public function testResetButton() + { + $this->assertEquals('', Html::resetButton()); + $this->assertEquals('', Html::resetButton('test', 'value', 'content<>', array('class' => 't'))); + } + + public function testInput() + { + $this->assertEquals('', Html::input('text')); + $this->assertEquals('', Html::input('text', 'test', 'value', array('class' => 't'))); + } + + public function testButtonInput() + { + } + + public function testSubmitInput() + { + } + + public function testResetInput() + { + } + + public function testTextInput() + { + } + + public function testHiddenInput() + { + } + + public function testPasswordInput() + { + } + + public function testFileInput() + { + } + + public function testTextarea() + { + } + + public function testRadio() + { + } + + public function testCheckbox() + { + } + + public function testDropDownList() + { + } + + public function testListBox() + { + } + + public function testCheckboxList() + { + } + + public function testRadioList() + { + } + + public function testRenderOptions() + { + } + + public function testRenderAttributes() + { + } + + public function testUrl() + { + } +} diff --git a/tests/unit/runtime/.gitignore b/tests/unit/runtime/.gitignore new file mode 100644 index 0000000..72e8ffc --- /dev/null +++ b/tests/unit/runtime/.gitignore @@ -0,0 +1 @@ +* From a5ababe4c63d8f22ca2b0b776cf5aacb9045e2dd Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Mon, 11 Mar 2013 15:03:00 -0400 Subject: [PATCH 63/84] Finished HtmlTest. --- framework/util/ArrayHelper.php | 4 +- framework/util/Html.php | 15 +-- framework/web/Controller.php | 4 - framework/web/Pagination.php | 4 +- framework/widgets/ActiveForm.php | 6 + tests/unit/framework/util/HtmlTest.php | 201 ++++++++++++++++++++++++++++++++- todo.md | 5 - 7 files changed, 218 insertions(+), 21 deletions(-) create mode 100644 framework/widgets/ActiveForm.php diff --git a/framework/util/ArrayHelper.php b/framework/util/ArrayHelper.php index ebf4b23..447d034 100644 --- a/framework/util/ArrayHelper.php +++ b/framework/util/ArrayHelper.php @@ -293,7 +293,7 @@ class ArrayHelper * @return array the encoded data * @see http://www.php.net/manual/en/function.htmlspecialchars.php */ - public static function htmlEncode($data, $valuesOnly = false, $charset = null) + public static function htmlEncode($data, $valuesOnly = true, $charset = null) { if ($charset === null) { $charset = Yii::$app->charset; @@ -322,7 +322,7 @@ class ArrayHelper * @return array the decoded data * @see http://www.php.net/manual/en/function.htmlspecialchars-decode.php */ - public static function htmlDecode($data, $valuesOnly = false) + public static function htmlDecode($data, $valuesOnly = true) { $d = array(); foreach ($data as $key => $value) { diff --git a/framework/util/Html.php b/framework/util/Html.php index 8c29047..92c8f02 100644 --- a/framework/util/Html.php +++ b/framework/util/Html.php @@ -106,6 +106,7 @@ class Html 'checked', 'readonly', 'disabled', + 'multiple', 'size', 'maxlength', @@ -652,7 +653,7 @@ class Html * except that the array keys represent the optgroup labels specified in $items. * @return string the generated drop-down list tag */ - public static function dropDownList($name, $items, $selection = null, $attributes = array()) + public static function dropDownList($name, $items = array(), $selection = null, $attributes = array()) { $attributes['name'] = $name; $options = static::renderOptions($items, $selection, $attributes); @@ -693,7 +694,7 @@ class Html * mode, we can still obtain the posted unselect value. * @return string the generated list box tag */ - public static function listBox($name, $items, $selection = null, $attributes = array()) + public static function listBox($name, $items = array(), $selection = null, $attributes = array()) { if (!isset($attributes['size'])) { $attributes['size'] = 4; @@ -742,7 +743,7 @@ class Html * value and the checked status of the checkbox input. * @return string the generated checkbox list */ - public static function checkboxList($name, $items, $selection = null, $options = array()) + public static function checkboxList($name, $items = array(), $selection = null, $options = array()) { if (substr($name, -2) !== '[]') { $name .= '[]'; @@ -800,7 +801,7 @@ class Html * value and the checked status of the radio button input. * @return string the generated radio button list */ - public static function radioList($name, $items, $selection = null, $options = array()) + public static function radioList($name, $items = array(), $selection = null, $options = array()) { $formatter = isset($options['item']) ? $options['item'] : null; $lines = array(); @@ -850,7 +851,7 @@ class Html { $lines = array(); if (isset($attributes['prompt'])) { - $prompt = strtr(static::encode($attributes['prompt']), ' ', ' '); + $prompt = str_replace(' ', ' ', static::encode($attributes['prompt'])); $lines[] = static::tag('option', $prompt, array('value' => '')); } @@ -863,7 +864,7 @@ class Html $groupAttrs = isset($groups[$key]) ? $groups[$key] : array(); $groupAttrs['label'] = $key; $attrs = array('options' => $options, 'groups' => $groups); - $content = static::renderOptions($selection, $value, $attrs); + $content = static::renderOptions($value, $selection, $attrs); $lines[] = static::tag('optgroup', "\n" . $content . "\n", $groupAttrs); } else { $attrs = isset($options[$key]) ? $options[$key] : array(); @@ -871,7 +872,7 @@ class Html $attrs['selected'] = $selection !== null && (!is_array($selection) && !strcmp($key, $selection) || is_array($selection) && in_array($key, $selection)); - $lines[] = static::tag('option', strtr(static::encode($value), ' ', ' '), $attrs); + $lines[] = static::tag('option', str_replace(' ', ' ', static::encode($value)), $attrs); } } diff --git a/framework/web/Controller.php b/framework/web/Controller.php index 9420034..2779c35 100644 --- a/framework/web/Controller.php +++ b/framework/web/Controller.php @@ -16,8 +16,4 @@ namespace yii\web; */ class Controller extends \yii\base\Controller { - public function createUrl($route, $params = array()) - { - - } } \ No newline at end of file diff --git a/framework/web/Pagination.php b/framework/web/Pagination.php index 84585d6..1d41c0c 100644 --- a/framework/web/Pagination.php +++ b/framework/web/Pagination.php @@ -14,7 +14,7 @@ use Yii; * * When data needs to be rendered in multiple pages, Pagination can be used to * represent information such as [[itemCount|total item count]], [[pageSize|page size]], - * [[page|current page]], etc. These information can be passed to [[yii\web\widgets\Pager|pagers]] + * [[page|current page]], etc. These information can be passed to [[yii\widgets\Pager|pagers]] * to render pagination buttons or links. * * The following example shows how to create a pagination object and feed it @@ -47,7 +47,7 @@ use Yii; * } * * // display pagination - * $this->widget('yii\web\widgets\LinkPager', array( + * $this->widget('yii\widgets\LinkPager', array( * 'pages' => $pages, * )); * ~~~ diff --git a/framework/widgets/ActiveForm.php b/framework/widgets/ActiveForm.php new file mode 100644 index 0000000..77fd9fd --- /dev/null +++ b/framework/widgets/ActiveForm.php @@ -0,0 +1,6 @@ +assertEquals('', Html::tag('input', '', array('type' => 'text', 'name' => 'test', 'value' => '<>'))); Html::$closeVoidElements = true; + + $this->assertEquals('', Html::tag('span', '', array('disabled' => true))); + Html::$showBooleanAttributeValues = false; + $this->assertEquals('', Html::tag('span', '', array('disabled' => true))); + Html::$showBooleanAttributeValues = true; } public function testBeginTag() @@ -166,69 +171,263 @@ class HtmlTest extends \yii\test\TestCase public function testButtonInput() { + $this->assertEquals('', Html::buttonInput('test')); + $this->assertEquals('', Html::buttonInput('test', 'text', array('class' => 'a'))); } public function testSubmitInput() { + $this->assertEquals('', Html::submitInput()); + $this->assertEquals('', Html::submitInput('test', 'text', array('class' => 'a'))); } public function testResetInput() { + $this->assertEquals('', Html::resetInput()); + $this->assertEquals('', Html::resetInput('test', 'text', array('class' => 'a'))); } public function testTextInput() { + $this->assertEquals('', Html::textInput('test')); + $this->assertEquals('', Html::textInput('test', 'value', array('class' => 't'))); } public function testHiddenInput() { + $this->assertEquals('', Html::hiddenInput('test')); + $this->assertEquals('', Html::hiddenInput('test', 'value', array('class' => 't'))); } public function testPasswordInput() { + $this->assertEquals('', Html::passwordInput('test')); + $this->assertEquals('', Html::passwordInput('test', 'value', array('class' => 't'))); } public function testFileInput() { + $this->assertEquals('', Html::fileInput('test')); + $this->assertEquals('', Html::fileInput('test', 'value', array('class' => 't'))); } public function testTextarea() { + $this->assertEquals('', Html::textarea('test')); + $this->assertEquals('', Html::textarea('test', 'value<>', array('class' => 't'))); } public function testRadio() { + $this->assertEquals('', Html::radio('test')); + $this->assertEquals('', Html::radio('test', null, true, array('class' => 'a'))); + $this->assertEquals('', Html::radio('test', null, true, array('class' => 'a' ,'uncheck' => '0'))); } public function testCheckbox() { + $this->assertEquals('', Html::checkbox('test')); + $this->assertEquals('', Html::checkbox('test', null, true, array('class' => 'a'))); + $this->assertEquals('', Html::checkbox('test', null, true, array('class' => 'a' ,'uncheck' => '0'))); } public function testDropDownList() { + $this->assertEquals("", Html::dropDownList('test')); + $this->assertEquals("", Html::dropDownList('test', $this->getDataItems())); + $this->assertEquals("", Html::dropDownList('test', $this->getDataItems(), 'value2')); } public function testListBox() { + $expected = << + + +EOD; + $this->assertEquals($expected, Html::listBox('test')); + $expected = << + + + +EOD; + $this->assertEquals($expected, Html::listBox('test', $this->getDataItems(), null, array('size' => 5))); + $expected = << + + + +EOD; + $this->assertEquals($expected, Html::listBox('test', $this->getDataItems2(), null)); + $expected = << + + + +EOD; + $this->assertEquals($expected, Html::listBox('test', $this->getDataItems(), 'value2')); + $expected = << + + + +EOD; + $this->assertEquals($expected, Html::listBox('test', $this->getDataItems(), array('value1', 'value2'))); + + $expected = << + + +EOD; + $this->assertEquals($expected, Html::listBox('test', array(), null, array('multiple' => true))); + $expected = << +EOD; + $this->assertEquals($expected, Html::listBox('test', array(), '', array('unselect' => '0'))); } public function testCheckboxList() { + $this->assertEquals('', Html::checkboxList('test')); + + $expected = << text1 + +EOD; + $this->assertEquals($expected, Html::checkboxList('test', $this->getDataItems(), array('value2'))); + + $expected = << text1<> + +EOD; + $this->assertEquals($expected, Html::checkboxList('test', $this->getDataItems2(), array('value2'))); + + $expected = <<
+ +EOD; + $this->assertEquals($expected, Html::checkboxList('test', $this->getDataItems(), array('value2'), array( + 'separator' => "
\n", + 'unselect' => '0', + ))); + + $expected = <<text1 + +EOD; + $this->assertEquals($expected, Html::checkboxList('test', $this->getDataItems(), array('value2'), array( + 'item' => function ($index, $label, $name, $value, $checked) { + return Html::label($label . ' ' . Html::checkbox($name, $value, $checked)); + } + ))); } public function testRadioList() { + $this->assertEquals('', Html::radioList('test')); + + $expected = << text1 + +EOD; + $this->assertEquals($expected, Html::radioList('test', $this->getDataItems(), array('value2'))); + + $expected = << text1<> + +EOD; + $this->assertEquals($expected, Html::radioList('test', $this->getDataItems2(), array('value2'))); + + $expected = <<
+ +EOD; + $this->assertEquals($expected, Html::radioList('test', $this->getDataItems(), array('value2'), array( + 'separator' => "
\n", + 'unselect' => '0', + ))); + + $expected = <<text1 + +EOD; + $this->assertEquals($expected, Html::radioList('test', $this->getDataItems(), array('value2'), array( + 'item' => function ($index, $label, $name, $value, $checked) { + return Html::label($label . ' ' . Html::radio($name, $value, $checked)); + } + ))); } public function testRenderOptions() { + $this->assertEquals('', Html::renderOptions(array())); + + $data = array( + 'value1' => 'label1', + 'group1' => array( + 'value11' => 'label11', + 'group11' => array( + 'value111' => 'label111', + ), + 'group12' => array(), + ), + 'value2' => 'label2', + 'group2' => array(), + ); + $expected = <<please select<> + + + + + + + + + + + + + + +EOD; + $attributes = array( + 'prompt' => 'please select<>', + 'options' => array( + 'value111' => array('class' => 'option'), + ), + 'groups' => array( + 'group12' => array('class' => 'group'), + ), + ); + $this->assertEquals($expected, Html::renderOptions($data, array('value111', 'value1'), $attributes)); } public function testRenderAttributes() { + $this->assertEquals('', Html::renderAttributes(array())); + $this->assertEquals(' name="test" value="1<>"', Html::renderAttributes(array('name' => 'test', 'empty' => null, 'value' => '1<>'))); + Html::$showBooleanAttributeValues = false; + $this->assertEquals(' checked disabled', Html::renderAttributes(array('checked' => 'checked', 'disabled' => true, 'hidden' => false))); + Html::$showBooleanAttributeValues = true; + } + + protected function getDataItems() + { + return array( + 'value1' => 'text1', + 'value2' => 'text2', + ); } - public function testUrl() + protected function getDataItems2() { + return array( + 'value1<>' => 'text1<>', + 'value 2' => 'text 2', + ); } } diff --git a/todo.md b/todo.md index 03b9f70..4d5343a 100644 --- a/todo.md +++ b/todo.md @@ -32,8 +32,6 @@ memo * module - Module should be able to define its own configuration including routes. Application should be able to overwrite it. * application - * security - - backport 1.1 changes - built-in console commands + api doc builder * support for markdown syntax @@ -46,7 +44,6 @@ memo * parsing?? * make dates/date patterns uniform application-wide including JUI, formats etc. - helpers - * array * image * string * file @@ -59,8 +56,6 @@ memo * move generation API out of gii, provide yiic commands to use it. Use same templates for gii/yiic. * i18n variant of templates * allow to generate module-specific CRUD -- markup and HTML helpers - * use HTML5 instead of XHTML - assets * ability to manage scripts order (store these in a vector?) * http://ryanbigg.com/guides/asset_pipeline.html, http://guides.rubyonrails.org/asset_pipeline.html, use content hash instead of mtime + directory hash. From 2422a134f007c2a17db6d62099cf43c262b1fda7 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Wed, 13 Mar 2013 16:21:13 -0400 Subject: [PATCH 64/84] Refactored Html. --- framework/util/Html.php | 322 +++++++++++++++++---------------- tests/unit/framework/util/HtmlTest.php | 66 ++++--- 2 files changed, 194 insertions(+), 194 deletions(-) diff --git a/framework/util/Html.php b/framework/util/Html.php index 92c8f02..bdbdcd2 100644 --- a/framework/util/Html.php +++ b/framework/util/Html.php @@ -153,15 +153,15 @@ class Html * @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 $attributes the element attributes. The values will be HTML-encoded using [[encode()]]. + * @param array $tagAttributes the element attributes. 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 HTML tag * @see beginTag * @see endTag */ - public static function tag($name, $content = '', $attributes = array()) + public static function tag($name, $content = '', $tagAttributes = array()) { - $html = '<' . $name . static::renderAttributes($attributes); + $html = '<' . $name . static::renderTagAttributes($tagAttributes); if (isset(static::$voidElements[strtolower($name)])) { return $html . (static::$closeVoidElements ? ' />' : '>'); } else { @@ -172,15 +172,15 @@ class Html /** * Generates a start tag. * @param string $name the tag name - * @param array $attributes the element attributes. The values will be HTML-encoded using [[encode()]]. + * @param array $tagAttributes the element attributes. 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 start tag * @see endTag * @see tag */ - public static function beginTag($name, $attributes = array()) + public static function beginTag($name, $tagAttributes = array()) { - return '<' . $name . static::renderAttributes($attributes) . '>'; + return '<' . $name . static::renderTagAttributes($tagAttributes) . '>'; } /** @@ -208,76 +208,76 @@ class Html /** * Generates a style tag. * @param string $content the style content - * @param array $attributes the attributes of the style tag. The values will be HTML-encoded using [[encode()]]. + * @param array $tagAttributes the attributes of the style tag. The values will be HTML-encoded using [[encode()]]. * Attributes whose value is null will be ignored and not put in the tag returned. * If the attributes does not contain "type", a default one with value "text/css" will be used. * @return string the generated style tag */ - public static function style($content, $attributes = array()) + public static function style($content, $tagAttributes = array()) { - if (!isset($attributes['type'])) { - $attributes['type'] = 'text/css'; + if (!isset($tagAttributes['type'])) { + $tagAttributes['type'] = 'text/css'; } - return static::tag('style', "/**/", $attributes); + return static::tag('style', "/**/", $tagAttributes); } /** * Generates a script tag. * @param string $content the script content - * @param array $attributes the attributes of the script tag. The values will be HTML-encoded using [[encode()]]. + * @param array $tagAttributes the attributes of the script tag. The values will be HTML-encoded using [[encode()]]. * Attributes whose value is null will be ignored and not put in the tag returned. * If the attributes does not contain "type", a default one with value "text/javascript" will be used. * @return string the generated script tag */ - public static function script($content, $attributes = array()) + public static function script($content, $tagAttributes = array()) { - if (!isset($attributes['type'])) { - $attributes['type'] = 'text/javascript'; + if (!isset($tagAttributes['type'])) { + $tagAttributes['type'] = 'text/javascript'; } - return static::tag('script', "/**/", $attributes); + return static::tag('script', "/**/", $tagAttributes); } /** * 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 $attributes the attributes of the link tag. The values will be HTML-encoded using [[encode()]]. + * @param array $tagAttributes the attributes of the link 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 link tag * @see url */ - public static function cssFile($url, $attributes = array()) + public static function cssFile($url, $tagAttributes = array()) { - $attributes['rel'] = 'stylesheet'; - $attributes['type'] = 'text/css'; - $attributes['href'] = static::url($url); - return static::tag('link', '', $attributes); + $tagAttributes['rel'] = 'stylesheet'; + $tagAttributes['type'] = 'text/css'; + $tagAttributes['href'] = static::url($url); + return static::tag('link', '', $tagAttributes); } /** * 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 $attributes the attributes of the script tag. The values will be HTML-encoded using [[encode()]]. + * @param array $tagAttributes the attributes of the script 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 script tag * @see url */ - public static function jsFile($url, $attributes = array()) + public static function jsFile($url, $tagAttributes = array()) { - $attributes['type'] = 'text/javascript'; - $attributes['src'] = static::url($url); - return static::tag('script', '', $attributes); + $tagAttributes['type'] = 'text/javascript'; + $tagAttributes['src'] = static::url($url); + return static::tag('script', '', $tagAttributes); } /** * Generates a form start tag. * @param array|string $action the form action URL. This parameter will be processed by [[url()]]. * @param string $method form method, either "post" or "get" (case-insensitive) - * @param array $attributes the attributes of the form tag. The values will be HTML-encoded using [[encode()]]. + * @param array $tagAttributes the attributes of the form 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 form start tag. * @see endForm */ - public static function beginForm($action = '', $method = 'post', $attributes = array()) + public static function beginForm($action = '', $method = 'post', $tagAttributes = array()) { $action = static::url($action); @@ -295,9 +295,9 @@ class Html $action = substr($action, 0, $pos); } - $attributes['action'] = $action; - $attributes['method'] = $method; - $form = static::beginTag('form', $attributes); + $tagAttributes['action'] = $action; + $tagAttributes['method'] = $method; + $form = static::beginTag('form', $tagAttributes); if ($hiddens !== array()) { $form .= "\n" . implode("\n", $hiddens); } @@ -323,17 +323,17 @@ class Html * @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 $attributes the attributes of the hyperlink tag. The values will be HTML-encoded using [[encode()]]. + * @param array $tagAttributes the attributes of the hyperlink 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 hyperlink * @see url */ - public static function a($text, $url = null, $attributes = array()) + public static function a($text, $url = null, $tagAttributes = array()) { if ($url !== null) { - $attributes['href'] = static::url($url); + $tagAttributes['href'] = static::url($url); } - return static::tag('a', $text, $attributes); + return static::tag('a', $text, $tagAttributes); } /** @@ -343,29 +343,29 @@ class Html * 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 $attributes the attributes of the hyperlink tag. The values will be HTML-encoded using [[encode()]]. + * @param array $tagAttributes the attributes of the hyperlink 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 mailto link */ - public static function mailto($text, $email = null, $attributes = array()) + public static function mailto($text, $email = null, $tagAttributes = array()) { - return static::a($text, 'mailto:' . ($email === null ? $text : $email), $attributes); + return static::a($text, 'mailto:' . ($email === null ? $text : $email), $tagAttributes); } /** * Generates an image tag. * @param string $src the image URL. This parameter will be processed by [[url()]]. - * @param array $attributes the attributes of the image tag. The values will be HTML-encoded using [[encode()]]. + * @param array $tagAttributes the attributes of the image 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 image tag */ - public static function img($src, $attributes = array()) + public static function img($src, $tagAttributes = array()) { - $attributes['src'] = static::url($src); - if (!isset($attributes['alt'])) { - $attributes['alt'] = ''; + $tagAttributes['src'] = static::url($src); + if (!isset($tagAttributes['alt'])) { + $tagAttributes['alt'] = ''; } - return static::tag('img', null, $attributes); + return static::tag('img', null, $tagAttributes); } /** @@ -375,14 +375,14 @@ class Html * 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 $attributes the attributes of the label tag. The values will be HTML-encoded using [[encode()]]. + * @param array $tagAttributes the attributes of the label 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 label tag */ - public static function label($content, $for = null, $attributes = array()) + public static function label($content, $for = null, $tagAttributes = array()) { - $attributes['for'] = $for; - return static::tag('label', $content, $attributes); + $tagAttributes['for'] = $for; + return static::tag('label', $content, $tagAttributes); } /** @@ -392,19 +392,19 @@ class Html * @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 $attributes the attributes of the button tag. The values will be HTML-encoded using [[encode()]]. + * @param array $tagAttributes 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. * If the attributes does not contain "type", a default one with value "button" will be used. * @return string the generated button tag */ - public static function button($name = null, $value = null, $content = 'Button', $attributes = array()) + public static function button($name = null, $value = null, $content = 'Button', $tagAttributes = array()) { - $attributes['name'] = $name; - $attributes['value'] = $value; - if (!isset($attributes['type'])) { - $attributes['type'] = 'button'; + $tagAttributes['name'] = $name; + $tagAttributes['value'] = $value; + if (!isset($tagAttributes['type'])) { + $tagAttributes['type'] = 'button'; } - return static::tag('button', $content, $attributes); + return static::tag('button', $content, $tagAttributes); } /** @@ -414,14 +414,14 @@ class Html * @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 $attributes the attributes of the button tag. The values will be HTML-encoded using [[encode()]]. + * @param array $tagAttributes 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 submit button tag */ - public static function submitButton($name = null, $value = null, $content = 'Submit', $attributes = array()) + public static function submitButton($name = null, $value = null, $content = 'Submit', $tagAttributes = array()) { - $attributes['type'] = 'submit'; - return static::button($name, $value, $content, $attributes); + $tagAttributes['type'] = 'submit'; + return static::button($name, $value, $content, $tagAttributes); } /** @@ -431,14 +431,14 @@ class Html * @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 $attributes the attributes of the button tag. The values will be HTML-encoded using [[encode()]]. + * @param array $tagAttributes 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 reset button tag */ - public static function resetButton($name = null, $value = null, $content = 'Reset', $attributes = array()) + public static function resetButton($name = null, $value = null, $content = 'Reset', $tagAttributes = array()) { - $attributes['type'] = 'reset'; - return static::button($name, $value, $content, $attributes); + $tagAttributes['type'] = 'reset'; + return static::button($name, $value, $content, $tagAttributes); } /** @@ -446,94 +446,94 @@ class Html * @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 $attributes the attributes of the input tag. The values will be HTML-encoded using [[encode()]]. + * @param array $tagAttributes the attributes of the input 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 input tag */ - public static function input($type, $name = null, $value = null, $attributes = array()) + public static function input($type, $name = null, $value = null, $tagAttributes = array()) { - $attributes['type'] = $type; - $attributes['name'] = $name; - $attributes['value'] = $value; - return static::tag('input', null, $attributes); + $tagAttributes['type'] = $type; + $tagAttributes['name'] = $name; + $tagAttributes['value'] = $value; + return static::tag('input', null, $tagAttributes); } /** * 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 $attributes the attributes of the button tag. The values will be HTML-encoded using [[encode()]]. + * @param array $tagAttributes 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 buttonInput($name, $value = 'Button', $attributes = array()) + public static function buttonInput($name, $value = 'Button', $tagAttributes = array()) { - return static::input('button', $name, $value, $attributes); + return static::input('button', $name, $value, $tagAttributes); } /** * 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 $attributes the attributes of the button tag. The values will be HTML-encoded using [[encode()]]. + * @param array $tagAttributes 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 submitInput($name = null, $value = 'Submit', $attributes = array()) + public static function submitInput($name = null, $value = 'Submit', $tagAttributes = array()) { - return static::input('submit', $name, $value, $attributes); + return static::input('submit', $name, $value, $tagAttributes); } /** * 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 $attributes the attributes of the button tag. The values will be HTML-encoded using [[encode()]]. + * @param array $tagAttributes 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', $attributes = array()) + public static function resetInput($name = null, $value = 'Reset', $tagAttributes = array()) { - return static::input('reset', $name, $value, $attributes); + return static::input('reset', $name, $value, $tagAttributes); } /** * 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 $attributes the attributes of the input tag. The values will be HTML-encoded using [[encode()]]. + * @param array $tagAttributes the attributes of the input 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 textInput($name, $value = null, $attributes = array()) + public static function textInput($name, $value = null, $tagAttributes = array()) { - return static::input('text', $name, $value, $attributes); + return static::input('text', $name, $value, $tagAttributes); } /** * 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 $attributes the attributes of the input tag. The values will be HTML-encoded using [[encode()]]. + * @param array $tagAttributes the attributes of the input 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 hiddenInput($name, $value = null, $attributes = array()) + public static function hiddenInput($name, $value = null, $tagAttributes = array()) { - return static::input('hidden', $name, $value, $attributes); + return static::input('hidden', $name, $value, $tagAttributes); } /** * 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 $attributes the attributes of the input tag. The values will be HTML-encoded using [[encode()]]. + * @param array $tagAttributes the attributes of the input 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 passwordInput($name, $value = null, $attributes = array()) + public static function passwordInput($name, $value = null, $tagAttributes = array()) { - return static::input('password', $name, $value, $attributes); + return static::input('password', $name, $value, $tagAttributes); } /** @@ -543,36 +543,36 @@ class Html * 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 $attributes the attributes of the input tag. The values will be HTML-encoded using [[encode()]]. + * @param array $tagAttributes the attributes of the input 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 fileInput($name, $value = null, $attributes = array()) + public static function fileInput($name, $value = null, $tagAttributes = array()) { - return static::input('file', $name, $value, $attributes); + return static::input('file', $name, $value, $tagAttributes); } /** * 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 $attributes the attributes of the input tag. The values will be HTML-encoded using [[encode()]]. + * @param array $tagAttributes the attributes of the input 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 text area tag */ - public static function textarea($name, $value = '', $attributes = array()) + public static function textarea($name, $value = '', $tagAttributes = array()) { - $attributes['name'] = $name; - return static::tag('textarea', static::encode($value), $attributes); + $tagAttributes['name'] = $name; + return static::tag('textarea', static::encode($value), $tagAttributes); } /** * Generates a radio button input. * @param string $name the name attribute. - * @param string $value the value attribute. If it is null, the value attribute will not be generated. * @param boolean $checked whether the radio button should be checked. - * @param array $attributes the attributes of the input tag. The values will be HTML-encoded using [[encode()]]. - * Attributes whose value is null will be ignored and not put in the tag returned. The following attribute + * @param string $value the value attribute. If it is null, the value attribute will not be rendered. + * @param array $tagAttributes the attributes of the input tag. The values will be HTML-encoded using [[encode()]]. + * Attributes whose value is null will be ignored and not put in the tag returned. The following attributes * will be specially handled and not put in the resulting tag: * * - uncheck: string, the value associated with the uncheck state of the radio button. When this attribute @@ -581,26 +581,27 @@ class Html * * @return string the generated radio button tag */ - public static function radio($name, $value = '1', $checked = false, $attributes = array()) + public static function radio($name, $checked = false, $value = '1', $tagAttributes = array()) { - $attributes['checked'] = $checked; - if (isset($attributes['uncheck'])) { + $tagAttributes['checked'] = $checked; + $tagAttributes['value'] = $value; + if (isset($tagAttributes['uncheck'])) { // add a hidden field so that if the radio button is not selected, it still submits a value - $hidden = static::hiddenInput($name, $attributes['uncheck']); - unset($attributes['uncheck']); + $hidden = static::hiddenInput($name, $tagAttributes['uncheck']); + unset($tagAttributes['uncheck']); } else { $hidden = ''; } - return $hidden . static::input('radio', $name, $value, $attributes); + return $hidden . static::input('radio', $name, $value, $tagAttributes); } /** * Generates a checkbox input. * @param string $name the name attribute. - * @param string $value the value attribute. If it is null, the value attribute will not be generated. * @param boolean $checked whether the checkbox should be checked. - * @param array $attributes the attributes of the input tag. The values will be HTML-encoded using [[encode()]]. - * Attributes whose value is null will be ignored and not put in the tag returned. The following attribute + * @param string $value the value attribute. If it is null, the value attribute will not be rendered. + * @param array $tagAttributes the attributes of the input tag. The values will be HTML-encoded using [[encode()]]. + * Attributes whose value is null will be ignored and not put in the tag returned. The following attributes * will be specially handled and not put in the resulting tag: * * - uncheck: string, the value associated with the uncheck state of the checkbox. When this attribute @@ -609,22 +610,24 @@ class Html * * @return string the generated checkbox tag */ - public static function checkbox($name, $value = '1', $checked = false, $attributes = array()) + public static function checkbox($name, $checked = false, $value = '1', $tagAttributes = array()) { - $attributes['checked'] = $checked; - if (isset($attributes['uncheck'])) { + $tagAttributes['checked'] = $checked; + $tagAttributes['value'] = $value; + if (isset($tagAttributes['uncheck'])) { // add a hidden field so that if the checkbox is not selected, it still submits a value - $hidden = static::hiddenInput($name, $attributes['uncheck']); - unset($attributes['uncheck']); + $hidden = static::hiddenInput($name, $tagAttributes['uncheck']); + unset($tagAttributes['uncheck']); } else { $hidden = ''; } - return $hidden . static::input('checkbox', $name, $value, $attributes); + return $hidden . static::input('checkbox', $name, $value, $tagAttributes); } /** * 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. @@ -633,9 +636,8 @@ class Html * * 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 string $selection the selected value - * @param array $attributes the attributes of the input tag. The values will be HTML-encoded using [[encode()]]. - * Attributes whose value is null will be ignored and not put in the tag returned. The following attribute + * @param array $tagAttributes the attributes of the input tag. The values will be HTML-encoded using [[encode()]]. + * Attributes whose value is null will be ignored and not put in the tag returned. The following attributes * will be specially handled and not put in the resulting tag: * * - prompt: string, a prompt text to be displayed as the first option; @@ -653,16 +655,17 @@ class Html * except that the array keys represent the optgroup labels specified in $items. * @return string the generated drop-down list tag */ - public static function dropDownList($name, $items = array(), $selection = null, $attributes = array()) + public static function dropDownList($name, $selection = null, $items = array(), $tagAttributes = array()) { - $attributes['name'] = $name; - $options = static::renderOptions($items, $selection, $attributes); - return static::tag('select', "\n" . $options . "\n", $attributes); + $tagAttributes['name'] = $name; + $options = static::renderSelectOptions($selection, $items, $tagAttributes); + return static::tag('select', "\n" . $options . "\n", $tagAttributes); } /** * 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. @@ -671,9 +674,8 @@ class Html * * 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 string|array $selection the selected value(s) - * @param array $attributes the attributes of the input tag. The values will be HTML-encoded using [[encode()]]. - * Attributes whose value is null will be ignored and not put in the tag returned. The following attribute + * @param array $tagAttributes the attributes of the input tag. The values will be HTML-encoded using [[encode()]]. + * Attributes whose value is null will be ignored and not put in the tag returned. The following attributes * will be specially handled and not put in the resulting tag: * * - prompt: string, a prompt text to be displayed as the first option; @@ -694,27 +696,27 @@ class Html * mode, we can still obtain the posted unselect value. * @return string the generated list box tag */ - public static function listBox($name, $items = array(), $selection = null, $attributes = array()) + public static function listBox($name, $selection = null, $items = array(), $tagAttributes = array()) { - if (!isset($attributes['size'])) { - $attributes['size'] = 4; + if (!isset($tagAttributes['size'])) { + $tagAttributes['size'] = 4; } - if (isset($attributes['multiple']) && $attributes['multiple'] && substr($name, -2) !== '[]') { + if (isset($tagAttributes['multiple']) && $tagAttributes['multiple'] && substr($name, -2) !== '[]') { $name .= '[]'; } - $attributes['name'] = $name; - if (isset($attributes['unselect'])) { + $tagAttributes['name'] = $name; + if (isset($tagAttributes['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, $attributes['unselect']); - unset($attributes['unselect']); + $hidden = static::hiddenInput($name, $tagAttributes['unselect']); + unset($tagAttributes['unselect']); } else { $hidden = ''; } - $options = static::renderOptions($items, $selection, $attributes); - return $hidden . static::tag('select', "\n" . $options . "\n", $attributes); + $options = static::renderSelectOptions($selection, $items, $tagAttributes); + return $hidden . static::tag('select', "\n" . $options . "\n", $tagAttributes); } /** @@ -722,10 +724,10 @@ class Html * 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 string|array $selection the selected value(s). * @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. @@ -735,7 +737,7 @@ class Html * corresponding to a single item in $items. The signature of this callback must be: * * ~~~ - * function ($index, $label, $name, $value, $checked) + * function ($index, $label, $name, $checked, $value) * ~~~ * * where $index is the zero-based index of the checkbox in the whole list; $label @@ -743,7 +745,7 @@ class Html * value and the checked status of the checkbox input. * @return string the generated checkbox list */ - public static function checkboxList($name, $items = array(), $selection = null, $options = array()) + public static function checkboxList($name, $selection = null, $items = array(), $options = array()) { if (substr($name, -2) !== '[]') { $name .= '[]'; @@ -757,9 +759,9 @@ class Html (!is_array($selection) && !strcmp($value, $selection) || is_array($selection) && in_array($value, $selection)); if ($formatter !== null) { - $lines[] = call_user_func($formatter, $index, $label, $name, $value, $checked); + $lines[] = call_user_func($formatter, $index, $label, $name, $checked, $value); } else { - $lines[] = static::label(static::checkbox($name, $value, $checked) . ' ' . $label); + $lines[] = static::label(static::checkbox($name, $checked, $value) . ' ' . $label); } $index++; } @@ -780,10 +782,10 @@ class Html * 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 string|array $selection the selected value(s). * @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. @@ -793,7 +795,7 @@ class Html * corresponding to a single item in $items. The signature of this callback must be: * * ~~~ - * function ($index, $label, $name, $value, $checked) + * function ($index, $label, $name, $checked, $value) * ~~~ * * where $index is the zero-based index of the radio button in the whole list; $label @@ -801,7 +803,7 @@ class Html * value and the checked status of the radio button input. * @return string the generated radio button list */ - public static function radioList($name, $items = array(), $selection = null, $options = array()) + public static function radioList($name, $selection = null, $items = array(), $options = array()) { $formatter = isset($options['item']) ? $options['item'] : null; $lines = array(); @@ -811,9 +813,9 @@ class Html (!is_array($selection) && !strcmp($value, $selection) || is_array($selection) && in_array($value, $selection)); if ($formatter !== null) { - $lines[] = call_user_func($formatter, $index, $label, $name, $value, $checked); + $lines[] = call_user_func($formatter, $index, $label, $name, $checked, $value); } else { - $lines[] = static::label(static::radio($name, $value, $checked) . ' ' . $label); + $lines[] = static::label(static::radio($name, $checked, $value) . ' ' . $label); } $index++; } @@ -831,6 +833,8 @@ class Html /** * 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. @@ -839,32 +843,30 @@ class Html * * 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 string|array $selection the selected value(s). This can be either a string for single selection - * or an array for multiple selections. - * @param array $attributes the attributes parameter that is passed to the [[dropDownList()]] or [[listBox()]] call. + * @param array $tagAttributes the attributes 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 renderOptions($items, $selection = null, &$attributes = array()) + public static function renderSelectOptions($selection, $items, &$tagAttributes = array()) { $lines = array(); - if (isset($attributes['prompt'])) { - $prompt = str_replace(' ', ' ', static::encode($attributes['prompt'])); + if (isset($tagAttributes['prompt'])) { + $prompt = str_replace(' ', ' ', static::encode($tagAttributes['prompt'])); $lines[] = static::tag('option', $prompt, array('value' => '')); } - $options = isset($attributes['options']) ? $attributes['options'] : array(); - $groups = isset($attributes['groups']) ? $attributes['groups'] : array(); - unset($attributes['prompt'], $attributes['options'], $attributes['groups']); + $options = isset($tagAttributes['options']) ? $tagAttributes['options'] : array(); + $groups = isset($tagAttributes['groups']) ? $tagAttributes['groups'] : array(); + unset($tagAttributes['prompt'], $tagAttributes['options'], $tagAttributes['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::renderOptions($value, $selection, $attrs); + $content = static::renderSelectOptions($selection, $value, $attrs); $lines[] = static::tag('optgroup', "\n" . $content . "\n", $groupAttrs); } else { $attrs = isset($options[$key]) ? $options[$key] : array(); @@ -883,26 +885,26 @@ class Html * 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()]]. + * @param array $tagAttributes 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 renderAttributes($attributes) + public static function renderTagAttributes($tagAttributes) { - if (count($attributes) > 1) { + if (count($tagAttributes) > 1) { $sorted = array(); foreach (static::$attributeOrder as $name) { - if (isset($attributes[$name])) { - $sorted[$name] = $attributes[$name]; + if (isset($tagAttributes[$name])) { + $sorted[$name] = $tagAttributes[$name]; } } - $attributes = array_merge($sorted, $attributes); + $tagAttributes = array_merge($sorted, $tagAttributes); } $html = ''; - foreach ($attributes as $name => $value) { + foreach ($tagAttributes as $name => $value) { if (isset(static::$booleanAttributes[strtolower($name)])) { if ($value || strcasecmp($name, $value) === 0) { $html .= static::$showBooleanAttributeValues ? " $name=\"$name\"" : " $name"; diff --git a/tests/unit/framework/util/HtmlTest.php b/tests/unit/framework/util/HtmlTest.php index 43991c4..35ae24c 100644 --- a/tests/unit/framework/util/HtmlTest.php +++ b/tests/unit/framework/util/HtmlTest.php @@ -220,22 +220,22 @@ class HtmlTest extends \yii\test\TestCase public function testRadio() { $this->assertEquals('', Html::radio('test')); - $this->assertEquals('', Html::radio('test', null, true, array('class' => 'a'))); - $this->assertEquals('', Html::radio('test', null, true, array('class' => 'a' ,'uncheck' => '0'))); + $this->assertEquals('', Html::radio('test', true, null, array('class' => 'a'))); + $this->assertEquals('', Html::radio('test', true, 2, array('class' => 'a' , 'uncheck' => '0'))); } public function testCheckbox() { $this->assertEquals('', Html::checkbox('test')); - $this->assertEquals('', Html::checkbox('test', null, true, array('class' => 'a'))); - $this->assertEquals('', Html::checkbox('test', null, true, array('class' => 'a' ,'uncheck' => '0'))); + $this->assertEquals('', Html::checkbox('test', true, null, array('class' => 'a'))); + $this->assertEquals('', Html::checkbox('test', true, 2, array('class' => 'a', 'uncheck' => '0'))); } public function testDropDownList() { $this->assertEquals("", Html::dropDownList('test')); - $this->assertEquals("", Html::dropDownList('test', $this->getDataItems())); - $this->assertEquals("", Html::dropDownList('test', $this->getDataItems(), 'value2')); + $this->assertEquals("", Html::dropDownList('test', null, $this->getDataItems())); + $this->assertEquals("", Html::dropDownList('test', 'value2', $this->getDataItems())); } public function testListBox() @@ -252,41 +252,41 @@ EOD; EOD; - $this->assertEquals($expected, Html::listBox('test', $this->getDataItems(), null, array('size' => 5))); + $this->assertEquals($expected, Html::listBox('test', null, $this->getDataItems(), array('size' => 5))); $expected = << EOD; - $this->assertEquals($expected, Html::listBox('test', $this->getDataItems2(), null)); + $this->assertEquals($expected, Html::listBox('test', null, $this->getDataItems2())); $expected = << EOD; - $this->assertEquals($expected, Html::listBox('test', $this->getDataItems(), 'value2')); + $this->assertEquals($expected, Html::listBox('test', 'value2', $this->getDataItems())); $expected = << EOD; - $this->assertEquals($expected, Html::listBox('test', $this->getDataItems(), array('value1', 'value2'))); + $this->assertEquals($expected, Html::listBox('test', array('value1', 'value2'), $this->getDataItems())); $expected = << EOD; - $this->assertEquals($expected, Html::listBox('test', array(), null, array('multiple' => true))); + $this->assertEquals($expected, Html::listBox('test', null, array(), array('multiple' => true))); $expected = << EOD; - $this->assertEquals($expected, Html::listBox('test', array(), '', array('unselect' => '0'))); + $this->assertEquals($expected, Html::listBox('test', '', array(), array('unselect' => '0'))); } public function testCheckboxList() @@ -297,30 +297,30 @@ EOD; EOD; - $this->assertEquals($expected, Html::checkboxList('test', $this->getDataItems(), array('value2'))); + $this->assertEquals($expected, Html::checkboxList('test', array('value2'), $this->getDataItems())); $expected = << text1<> EOD; - $this->assertEquals($expected, Html::checkboxList('test', $this->getDataItems2(), array('value2'))); + $this->assertEquals($expected, Html::checkboxList('test', array('value2'), $this->getDataItems2())); $expected = <<
EOD; - $this->assertEquals($expected, Html::checkboxList('test', $this->getDataItems(), array('value2'), array( + $this->assertEquals($expected, Html::checkboxList('test', array('value2'), $this->getDataItems(), array( 'separator' => "
\n", 'unselect' => '0', ))); $expected = <<text1 - +0 +1 EOD; - $this->assertEquals($expected, Html::checkboxList('test', $this->getDataItems(), array('value2'), array( - 'item' => function ($index, $label, $name, $value, $checked) { - return Html::label($label . ' ' . Html::checkbox($name, $value, $checked)); + $this->assertEquals($expected, Html::checkboxList('test', array('value2'), $this->getDataItems(), array( + 'item' => function ($index, $label, $name, $checked, $value) { + return $index . Html::label($label . ' ' . Html::checkbox($name, $checked, $value)); } ))); } @@ -333,38 +333,36 @@ EOD; EOD; - $this->assertEquals($expected, Html::radioList('test', $this->getDataItems(), array('value2'))); + $this->assertEquals($expected, Html::radioList('test', array('value2'), $this->getDataItems())); $expected = << text1<> EOD; - $this->assertEquals($expected, Html::radioList('test', $this->getDataItems2(), array('value2'))); + $this->assertEquals($expected, Html::radioList('test', array('value2'), $this->getDataItems2())); $expected = <<
EOD; - $this->assertEquals($expected, Html::radioList('test', $this->getDataItems(), array('value2'), array( + $this->assertEquals($expected, Html::radioList('test', array('value2'), $this->getDataItems(), array( 'separator' => "
\n", 'unselect' => '0', ))); $expected = <<text1 - +0 +1 EOD; - $this->assertEquals($expected, Html::radioList('test', $this->getDataItems(), array('value2'), array( - 'item' => function ($index, $label, $name, $value, $checked) { - return Html::label($label . ' ' . Html::radio($name, $value, $checked)); + $this->assertEquals($expected, Html::radioList('test', array('value2'), $this->getDataItems(), array( + 'item' => function ($index, $label, $name, $checked, $value) { + return $index . Html::label($label . ' ' . Html::radio($name, $checked, $value)); } ))); } public function testRenderOptions() { - $this->assertEquals('', Html::renderOptions(array())); - $data = array( 'value1' => 'label1', 'group1' => array( @@ -403,15 +401,15 @@ EOD; 'group12' => array('class' => 'group'), ), ); - $this->assertEquals($expected, Html::renderOptions($data, array('value111', 'value1'), $attributes)); + $this->assertEquals($expected, Html::renderSelectOptions(array('value111', 'value1'), $data, $attributes)); } public function testRenderAttributes() { - $this->assertEquals('', Html::renderAttributes(array())); - $this->assertEquals(' name="test" value="1<>"', Html::renderAttributes(array('name' => 'test', 'empty' => null, 'value' => '1<>'))); + $this->assertEquals('', Html::renderTagAttributes(array())); + $this->assertEquals(' name="test" value="1<>"', Html::renderTagAttributes(array('name' => 'test', 'empty' => null, 'value' => '1<>'))); Html::$showBooleanAttributeValues = false; - $this->assertEquals(' checked disabled', Html::renderAttributes(array('checked' => 'checked', 'disabled' => true, 'hidden' => false))); + $this->assertEquals(' checked disabled', Html::renderTagAttributes(array('checked' => 'checked', 'disabled' => true, 'hidden' => false))); Html::$showBooleanAttributeValues = true; } From 5227d2db7dcf94f022247cbeb41385d2e0896516 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Wed, 13 Mar 2013 16:46:52 -0400 Subject: [PATCH 65/84] refactoring. --- framework/util/Html.php | 389 ++++++++++++++++++--------------- tests/unit/framework/util/HtmlTest.php | 23 +- 2 files changed, 228 insertions(+), 184 deletions(-) diff --git a/framework/util/Html.php b/framework/util/Html.php index bdbdcd2..ebba40a 100644 --- a/framework/util/Html.php +++ b/framework/util/Html.php @@ -12,7 +12,7 @@ use yii\base\InvalidParamException; /** * Html provides a set of static methods for generating commonly used HTML tags. - * + * * @author Qiang Xue * @since 2.0 */ @@ -121,7 +121,6 @@ class Html 'media', ); - /** * Encodes special characters into HTML entities. * The [[yii\base\Application::charset|application charset]] will be used for encoding. @@ -153,15 +152,16 @@ class Html * @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 $tagAttributes the element attributes. The values will be HTML-encoded using [[encode()]]. - * Attributes whose value is null will be ignored and not put in the tag returned. + * @param array $options the tag options in terms of name-value pairs. These 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 = '', $tagAttributes = array()) + public static function tag($name, $content = '', $options = array()) { - $html = '<' . $name . static::renderTagAttributes($tagAttributes); + $html = '<' . $name . static::renderTagAttributes($options); if (isset(static::$voidElements[strtolower($name)])) { return $html . (static::$closeVoidElements ? ' />' : '>'); } else { @@ -172,15 +172,16 @@ class Html /** * Generates a start tag. * @param string $name the tag name - * @param array $tagAttributes the element attributes. The values will be HTML-encoded using [[encode()]]. - * Attributes whose value is null will be ignored and not put in the tag returned. + * @param array $options the tag options in terms of name-value pairs. These 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, $tagAttributes = array()) + public static function beginTag($name, $options = array()) { - return '<' . $name . static::renderTagAttributes($tagAttributes) . '>'; + return '<' . $name . static::renderTagAttributes($options) . '>'; } /** @@ -208,76 +209,81 @@ class Html /** * Generates a style tag. * @param string $content the style content - * @param array $tagAttributes the attributes of the style tag. The values will be HTML-encoded using [[encode()]]. - * Attributes whose value is null will be ignored and not put in the tag returned. - * If the attributes does not contain "type", a default one with value "text/css" will be used. + * @param array $options the tag options in terms of name-value pairs. These 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, $tagAttributes = array()) + public static function style($content, $options = array()) { - if (!isset($tagAttributes['type'])) { - $tagAttributes['type'] = 'text/css'; + if (!isset($options['type'])) { + $options['type'] = 'text/css'; } - return static::tag('style', "/**/", $tagAttributes); + return static::tag('style', "/**/", $options); } /** * Generates a script tag. * @param string $content the script content - * @param array $tagAttributes the attributes of the script tag. The values will be HTML-encoded using [[encode()]]. - * Attributes whose value is null will be ignored and not put in the tag returned. - * If the attributes does not contain "type", a default one with value "text/javascript" will be used. + * @param array $options the tag options in terms of name-value pairs. These 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, $tagAttributes = array()) + public static function script($content, $options = array()) { - if (!isset($tagAttributes['type'])) { - $tagAttributes['type'] = 'text/javascript'; + if (!isset($options['type'])) { + $options['type'] = 'text/javascript'; } - return static::tag('script', "/**/", $tagAttributes); + 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 $tagAttributes the attributes of the link tag. The values will be HTML-encoded using [[encode()]]. - * Attributes whose value is null will be ignored and not put in the tag returned. + * @param array $options the tag options in terms of name-value pairs. These 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, $tagAttributes = array()) + public static function cssFile($url, $options = array()) { - $tagAttributes['rel'] = 'stylesheet'; - $tagAttributes['type'] = 'text/css'; - $tagAttributes['href'] = static::url($url); - return static::tag('link', '', $tagAttributes); + $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 $tagAttributes the attributes of the script tag. The values will be HTML-encoded using [[encode()]]. - * Attributes whose value is null will be ignored and not put in the tag returned. + * @param array $options the tag options in terms of name-value pairs. These 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, $tagAttributes = array()) + public static function jsFile($url, $options = array()) { - $tagAttributes['type'] = 'text/javascript'; - $tagAttributes['src'] = static::url($url); - return static::tag('script', '', $tagAttributes); + $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 form method, either "post" or "get" (case-insensitive) - * @param array $tagAttributes the attributes of the form tag. The values will be HTML-encoded using [[encode()]]. - * Attributes whose value is null will be ignored and not put in the tag returned. + * @param array $options the tag options in terms of name-value pairs. These 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', $tagAttributes = array()) + public static function beginForm($action = '', $method = 'post', $options = array()) { $action = static::url($action); @@ -295,9 +301,9 @@ class Html $action = substr($action, 0, $pos); } - $tagAttributes['action'] = $action; - $tagAttributes['method'] = $method; - $form = static::beginTag('form', $tagAttributes); + $options['action'] = $action; + $options['method'] = $method; + $form = static::beginTag('form', $options); if ($hiddens !== array()) { $form .= "\n" . implode("\n", $hiddens); } @@ -323,17 +329,18 @@ class Html * @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 $tagAttributes the attributes of the hyperlink tag. The values will be HTML-encoded using [[encode()]]. - * Attributes whose value is null will be ignored and not put in the tag returned. + * @param array $options the tag options in terms of name-value pairs. These 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, $tagAttributes = array()) + public static function a($text, $url = null, $options = array()) { if ($url !== null) { - $tagAttributes['href'] = static::url($url); + $options['href'] = static::url($url); } - return static::tag('a', $text, $tagAttributes); + return static::tag('a', $text, $options); } /** @@ -343,29 +350,31 @@ class Html * 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 $tagAttributes the attributes of the hyperlink tag. The values will be HTML-encoded using [[encode()]]. - * Attributes whose value is null will be ignored and not put in the tag returned. + * @param array $options the tag options in terms of name-value pairs. These 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, $tagAttributes = array()) + public static function mailto($text, $email = null, $options = array()) { - return static::a($text, 'mailto:' . ($email === null ? $text : $email), $tagAttributes); + 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 $tagAttributes the attributes of the image tag. The values will be HTML-encoded using [[encode()]]. - * Attributes whose value is null will be ignored and not put in the tag returned. + * @param array $options the tag options in terms of name-value pairs. These 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, $tagAttributes = array()) + public static function img($src, $options = array()) { - $tagAttributes['src'] = static::url($src); - if (!isset($tagAttributes['alt'])) { - $tagAttributes['alt'] = ''; + $options['src'] = static::url($src); + if (!isset($options['alt'])) { + $options['alt'] = ''; } - return static::tag('img', null, $tagAttributes); + return static::tag('img', null, $options); } /** @@ -375,14 +384,15 @@ class Html * 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 $tagAttributes the attributes of the label tag. The values will be HTML-encoded using [[encode()]]. - * Attributes whose value is null will be ignored and not put in the tag returned. + * @param array $options the tag options in terms of name-value pairs. These 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, $tagAttributes = array()) + public static function label($content, $for = null, $options = array()) { - $tagAttributes['for'] = $for; - return static::tag('label', $content, $tagAttributes); + $options['for'] = $for; + return static::tag('label', $content, $options); } /** @@ -392,19 +402,20 @@ class Html * @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 $tagAttributes 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. - * If the attributes does not contain "type", a default one with value "button" will be used. + * @param array $options the tag options in terms of name-value pairs. These 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', $tagAttributes = array()) + public static function button($name = null, $value = null, $content = 'Button', $options = array()) { - $tagAttributes['name'] = $name; - $tagAttributes['value'] = $value; - if (!isset($tagAttributes['type'])) { - $tagAttributes['type'] = 'button'; + $options['name'] = $name; + $options['value'] = $value; + if (!isset($options['type'])) { + $options['type'] = 'button'; } - return static::tag('button', $content, $tagAttributes); + return static::tag('button', $content, $options); } /** @@ -414,14 +425,15 @@ class Html * @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 $tagAttributes 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. + * @param array $options the tag options in terms of name-value pairs. These 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', $tagAttributes = array()) + public static function submitButton($name = null, $value = null, $content = 'Submit', $options = array()) { - $tagAttributes['type'] = 'submit'; - return static::button($name, $value, $content, $tagAttributes); + $options['type'] = 'submit'; + return static::button($name, $value, $content, $options); } /** @@ -431,14 +443,15 @@ class Html * @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 $tagAttributes 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. + * @param array $options the tag options in terms of name-value pairs. These 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', $tagAttributes = array()) + public static function resetButton($name = null, $value = null, $content = 'Reset', $options = array()) { - $tagAttributes['type'] = 'reset'; - return static::button($name, $value, $content, $tagAttributes); + $options['type'] = 'reset'; + return static::button($name, $value, $content, $options); } /** @@ -446,94 +459,100 @@ class Html * @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 $tagAttributes the attributes of the input tag. The values will be HTML-encoded using [[encode()]]. - * Attributes whose value is null will be ignored and not put in the tag returned. + * @param array $options the tag options in terms of name-value pairs. These 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, $tagAttributes = array()) + public static function input($type, $name = null, $value = null, $options = array()) { - $tagAttributes['type'] = $type; - $tagAttributes['name'] = $name; - $tagAttributes['value'] = $value; - return static::tag('input', null, $tagAttributes); + $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 $tagAttributes 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. + * @param array $options the tag options in terms of name-value pairs. These 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', $tagAttributes = array()) + public static function buttonInput($name, $value = 'Button', $options = array()) { - return static::input('button', $name, $value, $tagAttributes); + 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 $tagAttributes 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. + * @param array $options the tag options in terms of name-value pairs. These 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', $tagAttributes = array()) + public static function submitInput($name = null, $value = 'Submit', $options = array()) { - return static::input('submit', $name, $value, $tagAttributes); + 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 $tagAttributes the attributes of the button tag. The values will be HTML-encoded using [[encode()]]. + * @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', $tagAttributes = array()) + public static function resetInput($name = null, $value = 'Reset', $options = array()) { - return static::input('reset', $name, $value, $tagAttributes); + 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 $tagAttributes the attributes of the input tag. The values will be HTML-encoded using [[encode()]]. - * Attributes whose value is null will be ignored and not put in the tag returned. + * @param array $options the tag options in terms of name-value pairs. These 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, $tagAttributes = array()) + public static function textInput($name, $value = null, $options = array()) { - return static::input('text', $name, $value, $tagAttributes); + 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 $tagAttributes the attributes of the input tag. The values will be HTML-encoded using [[encode()]]. - * Attributes whose value is null will be ignored and not put in the tag returned. + * @param array $options the tag options in terms of name-value pairs. These 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, $tagAttributes = array()) + public static function hiddenInput($name, $value = null, $options = array()) { - return static::input('hidden', $name, $value, $tagAttributes); + 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 $tagAttributes the attributes of the input tag. The values will be HTML-encoded using [[encode()]]. - * Attributes whose value is null will be ignored and not put in the tag returned. + * @param array $options the tag options in terms of name-value pairs. These 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, $tagAttributes = array()) + public static function passwordInput($name, $value = null, $options = array()) { - return static::input('password', $name, $value, $tagAttributes); + return static::input('password', $name, $value, $options); } /** @@ -543,27 +562,29 @@ class Html * 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 $tagAttributes the attributes of the input tag. The values will be HTML-encoded using [[encode()]]. - * Attributes whose value is null will be ignored and not put in the tag returned. + * @param array $options the tag options in terms of name-value pairs. These 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, $tagAttributes = array()) + public static function fileInput($name, $value = null, $options = array()) { - return static::input('file', $name, $value, $tagAttributes); + 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 $tagAttributes the attributes of the input tag. The values will be HTML-encoded using [[encode()]]. - * Attributes whose value is null will be ignored and not put in the tag returned. + * @param array $options the tag options in terms of name-value pairs. These 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 = '', $tagAttributes = array()) + public static function textarea($name, $value = '', $options = array()) { - $tagAttributes['name'] = $name; - return static::tag('textarea', static::encode($value), $tagAttributes); + $options['name'] = $name; + return static::tag('textarea', static::encode($value), $options); } /** @@ -571,28 +592,29 @@ class Html * @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 $tagAttributes the attributes of the input tag. The values will be HTML-encoded using [[encode()]]. - * Attributes whose value is null will be ignored and not put in the tag returned. The following attributes - * will be specially handled and not put in the resulting tag: + * @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', $tagAttributes = array()) + public static function radio($name, $checked = false, $value = '1', $options = array()) { - $tagAttributes['checked'] = $checked; - $tagAttributes['value'] = $value; - if (isset($tagAttributes['uncheck'])) { + $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, $tagAttributes['uncheck']); - unset($tagAttributes['uncheck']); + $hidden = static::hiddenInput($name, $options['uncheck']); + unset($options['uncheck']); } else { $hidden = ''; } - return $hidden . static::input('radio', $name, $value, $tagAttributes); + return $hidden . static::input('radio', $name, $value, $options); } /** @@ -600,28 +622,29 @@ class Html * @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 $tagAttributes the attributes of the input tag. The values will be HTML-encoded using [[encode()]]. - * Attributes whose value is null will be ignored and not put in the tag returned. The following attributes - * will be specially handled and not put in the resulting tag: + * @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', $tagAttributes = array()) + public static function checkbox($name, $checked = false, $value = '1', $options = array()) { - $tagAttributes['checked'] = $checked; - $tagAttributes['value'] = $value; - if (isset($tagAttributes['uncheck'])) { + $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, $tagAttributes['uncheck']); - unset($tagAttributes['uncheck']); + $hidden = static::hiddenInput($name, $options['uncheck']); + unset($options['uncheck']); } else { $hidden = ''; } - return $hidden . static::input('checkbox', $name, $value, $tagAttributes); + return $hidden . static::input('checkbox', $name, $value, $options); } /** @@ -636,12 +659,10 @@ class Html * * 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 $tagAttributes the attributes of the input tag. The values will be HTML-encoded using [[encode()]]. - * Attributes whose value is null will be ignored and not put in the tag returned. The following attributes - * will be specially handled and not put in the resulting tag: + * @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 option tags. The array keys must be valid option values, + * - 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, * * ~~~ @@ -653,13 +674,17 @@ class Html * * - 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(), $tagAttributes = array()) + public static function dropDownList($name, $selection = null, $items = array(), $options = array()) { - $tagAttributes['name'] = $name; - $options = static::renderSelectOptions($selection, $items, $tagAttributes); - return static::tag('select', "\n" . $options . "\n", $tagAttributes); + $options['name'] = $name; + $selectOptions = static::renderSelectOptions($selection, $items, $options); + return static::tag('select', "\n" . $selectOptions . "\n", $options); } /** @@ -674,12 +699,10 @@ class Html * * 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 $tagAttributes the attributes of the input tag. The values will be HTML-encoded using [[encode()]]. - * Attributes whose value is null will be ignored and not put in the tag returned. The following attributes - * will be specially handled and not put in the resulting tag: + * @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 option tags. The array keys must be valid option values, + * - 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, * * ~~~ @@ -694,29 +717,33 @@ class Html * - 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(), $tagAttributes = array()) + public static function listBox($name, $selection = null, $items = array(), $options = array()) { - if (!isset($tagAttributes['size'])) { - $tagAttributes['size'] = 4; + if (!isset($options['size'])) { + $options['size'] = 4; } - if (isset($tagAttributes['multiple']) && $tagAttributes['multiple'] && substr($name, -2) !== '[]') { + if (isset($options['multiple']) && $options['multiple'] && substr($name, -2) !== '[]') { $name .= '[]'; } - $tagAttributes['name'] = $name; - if (isset($tagAttributes['unselect'])) { + $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, $tagAttributes['unselect']); - unset($tagAttributes['unselect']); + $hidden = static::hiddenInput($name, $options['unselect']); + unset($options['unselect']); } else { $hidden = ''; } - $options = static::renderSelectOptions($selection, $items, $tagAttributes); - return $hidden . static::tag('select', "\n" . $options . "\n", $tagAttributes); + $selectOptions = static::renderSelectOptions($selection, $items, $options); + return $hidden . static::tag('select', "\n" . $selectOptions . "\n", $options); } /** @@ -757,7 +784,7 @@ class Html foreach ($items as $value => $label) { $checked = $selection !== null && (!is_array($selection) && !strcmp($value, $selection) - || is_array($selection) && in_array($value, $selection)); + || is_array($selection) && in_array($value, $selection)); if ($formatter !== null) { $lines[] = call_user_func($formatter, $index, $label, $name, $checked, $value); } else { @@ -811,7 +838,7 @@ class Html foreach ($items as $value => $label) { $checked = $selection !== null && (!is_array($selection) && !strcmp($value, $selection) - || is_array($selection) && in_array($value, $selection)); + || is_array($selection) && in_array($value, $selection)); if ($formatter !== null) { $lines[] = call_user_func($formatter, $index, $label, $name, $checked, $value); } else { @@ -843,23 +870,23 @@ class Html * * 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 $tagAttributes the attributes parameter that is passed to the [[dropDownList()]] or [[listBox()]] call. + * @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, &$tagAttributes = array()) + public static function renderSelectOptions($selection, $items, &$tagOptions = array()) { $lines = array(); - if (isset($tagAttributes['prompt'])) { - $prompt = str_replace(' ', ' ', static::encode($tagAttributes['prompt'])); + if (isset($tagOptions['prompt'])) { + $prompt = str_replace(' ', ' ', static::encode($tagOptions['prompt'])); $lines[] = static::tag('option', $prompt, array('value' => '')); } - $options = isset($tagAttributes['options']) ? $tagAttributes['options'] : array(); - $groups = isset($tagAttributes['groups']) ? $tagAttributes['groups'] : array(); - unset($tagAttributes['prompt'], $tagAttributes['options'], $tagAttributes['groups']); + $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)) { @@ -873,7 +900,7 @@ class Html $attrs['value'] = $key; $attrs['selected'] = $selection !== null && (!is_array($selection) && !strcmp($key, $selection) - || is_array($selection) && in_array($key, $selection)); + || is_array($selection) && in_array($key, $selection)); $lines[] = static::tag('option', str_replace(' ', ' ', static::encode($value)), $attrs); } } @@ -885,26 +912,26 @@ class Html * Renders the HTML tag attributes. * Boolean attributes such as s 'checked', 'disabled', 'readonly', will be handled specially * according to [[booleanAttributes]] and [[showBooleanAttributeValues]]. - * @param array $tagAttributes attributes to be rendered. The attribute values will be HTML-encoded using [[encode()]]. + * @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($tagAttributes) + public static function renderTagAttributes($attributes) { - if (count($tagAttributes) > 1) { + if (count($attributes) > 1) { $sorted = array(); foreach (static::$attributeOrder as $name) { - if (isset($tagAttributes[$name])) { - $sorted[$name] = $tagAttributes[$name]; + if (isset($attributes[$name])) { + $sorted[$name] = $attributes[$name]; } } - $tagAttributes = array_merge($sorted, $tagAttributes); + $attributes = array_merge($sorted, $attributes); } $html = ''; - foreach ($tagAttributes as $name => $value) { + foreach ($attributes as $name => $value) { if (isset(static::$booleanAttributes[strtolower($name)])) { if ($value || strcasecmp($name, $value) === 0) { $html .= static::$showBooleanAttributeValues ? " $name=\"$name\"" : " $name"; diff --git a/tests/unit/framework/util/HtmlTest.php b/tests/unit/framework/util/HtmlTest.php index 35ae24c..a887f29 100644 --- a/tests/unit/framework/util/HtmlTest.php +++ b/tests/unit/framework/util/HtmlTest.php @@ -233,9 +233,26 @@ class HtmlTest extends \yii\test\TestCase public function testDropDownList() { - $this->assertEquals("", Html::dropDownList('test')); - $this->assertEquals("", Html::dropDownList('test', null, $this->getDataItems())); - $this->assertEquals("", Html::dropDownList('test', 'value2', $this->getDataItems())); + $expected = << + + +EOD; + $this->assertEquals($expected, Html::dropDownList('test')); + $expected = << + + + +EOD; + $this->assertEquals($expected, Html::dropDownList('test', null, $this->getDataItems())); + $expected = << + + + +EOD; + $this->assertEquals($expected, Html::dropDownList('test', 'value2', $this->getDataItems())); } public function testListBox() From c69e61801149d97992d0ddd6580a483cb668f1d7 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Wed, 13 Mar 2013 17:25:27 -0400 Subject: [PATCH 66/84] ActiveForm WIP --- framework/widgets/ActiveForm.php | 128 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 127 insertions(+), 1 deletion(-) diff --git a/framework/widgets/ActiveForm.php b/framework/widgets/ActiveForm.php index 77fd9fd..2bac53a 100644 --- a/framework/widgets/ActiveForm.php +++ b/framework/widgets/ActiveForm.php @@ -1,6 +1,132 @@ + * @since 2.0 + */ +class ActiveForm extends Widget { + /** + * @var mixed the form action URL (see {@link CHtml::normalizeUrl} for details about this parameter). + * If not set, the current page URL is used. + */ + public $action = ''; + /** + * @var string the form submission method. This should be either 'post' or 'get'. + * Defaults to 'post'. + */ + public $method = 'post'; + /** + * @var string the CSS class name for error messages. Defaults to 'errorMessage'. + * Individual {@link error} call may override this value by specifying the 'class' HTML option. + */ + public $errorMessageCssClass = 'errorMessage'; + /** + * @var array additional HTML attributes that should be rendered for the form tag. + */ + public $htmlOptions = array(); + /** + * @var boolean whether to enable data validation via AJAX. Defaults to false. + * When this property is set true, you should respond to the AJAX validation request on the server side as shown below: + *
+	 * public function actionCreate()
+	 * {
+	 *     $model=new User;
+	 *     if(isset($_POST['ajax']) && $_POST['ajax']==='user-form')
+	 *     {
+	 *         echo CActiveForm::validate($model);
+	 *         Yii::app()->end();
+	 *     }
+	 *     ......
+	 * }
+	 * 
+ */ + public $enableAjaxValidation = false; + /** + * @var boolean whether to enable client-side data validation. Defaults to false. + * + * When this property is set true, client-side validation will be performed by validators + * that support it (see {@link CValidator::enableClientValidation} and {@link CValidator::clientValidateAttribute}). + * + * @see error + * @since 1.1.7 + */ + public $enableClientValidation = false; + + + public function errorSummary($model, $options = array()) + { + } + + public function error($model, $attribute, $options = array()) + { + } + + public function label($model, $attribute, $options = array()) + { + } + + public function input($type, $model, $attribute, $options = array()) + { + return ''; + } + + public function textInput($model, $attribute, $options = array()) + { + return $this->input('text', $model, $attribute, $options); + } + + public function hiddenInput($model, $attribute, $options = array()) + { + return $this->input('hidden', $model, $attribute, $options); + } + + public function passwordInput($model, $attribute, $options = array()) + { + return $this->input('password', $model, $attribute, $options); + } + + public function fileInput($model, $attribute, $options = array()) + { + return $this->input('file', $model, $attribute, $options); + } + + public function textarea($model, $attribute, $options = array()) + { + } + + public function radio($model, $attribute, $value = '1', $options = array()) + { + } + + public function checkbox($model, $attribute, $value = '1', $options = array()) + { + } + + public function dropDownList($model, $attribute, $items, $options = array()) + { + } + + public function listBox($model, $attribute, $items, $options = array()) + { + } + + public function checkboxList($model, $attribute, $items, $options = array()) + { + } + public function radioList($model, $attribute, $items, $options = array()) + { + } } From b71e83016fedf1c874f3514346f720db6724feea Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Thu, 14 Mar 2013 20:44:58 -0400 Subject: [PATCH 67/84] ActiveForm WIP --- framework/base/Model.php | 40 ++++++++-------- framework/util/Html.php | 46 +++++++++--------- framework/widgets/ActiveForm.php | 100 +++++++++++++++++++++++++++------------ 3 files changed, 114 insertions(+), 72 deletions(-) diff --git a/framework/base/Model.php b/framework/base/Model.php index 50b1058..b761ada 100644 --- a/framework/base/Model.php +++ b/framework/base/Model.php @@ -420,12 +420,31 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess } /** + * Returns the first error of every attribute in the model. + * @return array the first errors. An empty array will be returned if there is no error. + */ + public function getFirstErrors() + { + if (empty($this->_errors)) { + return array(); + } else { + $errors = array(); + foreach ($this->_errors as $errors) { + if (isset($errors[0])) { + $errors[] = $errors[0]; + } + } + } + return $errors; + } + + /** * Returns the first error of the specified attribute. * @param string $attribute attribute name. * @return string the error message. Null is returned if no error. * @see getErrors */ - public function getError($attribute) + public function getFirstError($attribute) { return isset($this->_errors[$attribute]) ? reset($this->_errors[$attribute]) : null; } @@ -441,25 +460,6 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess } /** - * Adds a list of errors. - * @param array $errors a list of errors. The array keys must be attribute names. - * The array values should be error messages. If an attribute has multiple errors, - * these errors must be given in terms of an array. - */ - public function addErrors($errors) - { - foreach ($errors as $attribute => $error) { - if (is_array($error)) { - foreach ($error as $e) { - $this->_errors[$attribute][] = $e; - } - } else { - $this->_errors[$attribute][] = $error; - } - } - } - - /** * Removes errors for all attributes or a single attribute. * @param string $attribute attribute name. Use null to remove errors for all attribute. */ diff --git a/framework/util/Html.php b/framework/util/Html.php index ebba40a..a7b744b 100644 --- a/framework/util/Html.php +++ b/framework/util/Html.php @@ -152,7 +152,7 @@ class Html * @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 be rendered as + * @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 @@ -172,7 +172,7 @@ class Html /** * Generates a start tag. * @param string $name the tag name - * @param array $options the tag options in terms of name-value pairs. These be rendered as + * @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 @@ -209,7 +209,7 @@ class Html /** * Generates a style tag. * @param string $content the style content - * @param array $options the tag options in terms of name-value pairs. These be rendered as + * @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. @@ -226,7 +226,7 @@ class Html /** * Generates a script tag. * @param string $content the script content - * @param array $options the tag options in terms of name-value pairs. These be rendered as + * @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. @@ -243,7 +243,7 @@ class Html /** * 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 be rendered as + * @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 @@ -260,7 +260,7 @@ class Html /** * 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 be rendered as + * @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 @@ -276,8 +276,8 @@ class Html /** * Generates a form start tag. * @param array|string $action the form action URL. This parameter will be processed by [[url()]]. - * @param string $method form method, either "post" or "get" (case-insensitive) - * @param array $options the tag options in terms of name-value pairs. These be rendered as + * @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. @@ -329,7 +329,7 @@ class Html * @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 be rendered as + * @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 @@ -350,7 +350,7 @@ class Html * 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 be rendered as + * @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 @@ -363,7 +363,7 @@ class Html /** * 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 be rendered as + * @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 @@ -384,7 +384,7 @@ class Html * 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 be rendered as + * @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 @@ -402,7 +402,7 @@ class Html * @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 be rendered as + * @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. @@ -425,7 +425,7 @@ class Html * @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 be rendered as + * @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 @@ -443,7 +443,7 @@ class Html * @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 be rendered as + * @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 @@ -459,7 +459,7 @@ class Html * @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 be rendered as + * @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 @@ -476,7 +476,7 @@ class Html * 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 be rendered as + * @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 @@ -490,7 +490,7 @@ class Html * 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 be rendered as + * @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 @@ -517,7 +517,7 @@ class Html * 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 be rendered as + * @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 @@ -531,7 +531,7 @@ class Html * 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 be rendered as + * @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 @@ -545,7 +545,7 @@ class Html * 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 be rendered as + * @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 @@ -562,7 +562,7 @@ class Html * 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 be rendered as + * @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 @@ -576,7 +576,7 @@ class Html * 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 be rendered as + * @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 diff --git a/framework/widgets/ActiveForm.php b/framework/widgets/ActiveForm.php index 2bac53a..4a0f89c 100644 --- a/framework/widgets/ActiveForm.php +++ b/framework/widgets/ActiveForm.php @@ -7,7 +7,11 @@ namespace yii\widgets; +use Yii; use yii\base\Widget; +use yii\base\Model; +use yii\util\Html; +use yii\util\ArrayHelper; /** * ActiveForm ... @@ -18,8 +22,7 @@ use yii\base\Widget; class ActiveForm extends Widget { /** - * @var mixed the form action URL (see {@link CHtml::normalizeUrl} for details about this parameter). - * If not set, the current page URL is used. + * @param array|string $action the form action URL. This parameter will be processed by [[\yii\util\Html::url()]]. */ public $action = ''; /** @@ -28,49 +31,88 @@ class ActiveForm extends Widget */ public $method = 'post'; /** - * @var string the CSS class name for error messages. Defaults to 'errorMessage'. - * Individual {@link error} call may override this value by specifying the 'class' HTML option. + * @var string the default CSS class for the error summary container. + * @see errorSummary() */ - public $errorMessageCssClass = 'errorMessage'; + public $errorSummaryClass = 'yii-error-summary'; /** - * @var array additional HTML attributes that should be rendered for the form tag. + * @var string the default CSS class that indicates an input has error. + * This is */ - public $htmlOptions = array(); - /** - * @var boolean whether to enable data validation via AJAX. Defaults to false. - * When this property is set true, you should respond to the AJAX validation request on the server side as shown below: - *
-	 * public function actionCreate()
-	 * {
-	 *     $model=new User;
-	 *     if(isset($_POST['ajax']) && $_POST['ajax']==='user-form')
-	 *     {
-	 *         echo CActiveForm::validate($model);
-	 *         Yii::app()->end();
-	 *     }
-	 *     ......
-	 * }
-	 * 
- */ - public $enableAjaxValidation = false; + public $errorClass = 'yii-error'; + public $successClass = 'yii-success'; + public $validatingClass = 'yii-validating'; /** * @var boolean whether to enable client-side data validation. Defaults to false. - * * When this property is set true, client-side validation will be performed by validators * that support it (see {@link CValidator::enableClientValidation} and {@link CValidator::clientValidateAttribute}). - * - * @see error - * @since 1.1.7 */ public $enableClientValidation = false; + public $options = array(); + - public function errorSummary($model, $options = array()) + /** + * @param Model|Model[] $models + * @param array $options + * @return string + */ + public function errorSummary($models, $options = array()) { + if (!is_array($models)) { + $models = array($models); + } + + $showAll = isset($options['showAll']) && $options['showAll']); + $lines = array(); + /** @var $model Model */ + foreach ($models as $model) { + if ($showAll) { + foreach ($model->getErrors() as $errors) { + $lines = array_merge($lines, $errors); + } + } else { + $lines = array_merge($lines, $model->getFirstErrors()); + } + } + + $header = isset($options['header']) ? $options['header'] : '

' . Yii::t('yii|Please fix the following errors:') . '

'; + $footer = isset($options['footer']) ? $options['footer'] : ''; + $container = isset($options['container']) ? $options['container'] : 'div'; + unset($options['showAll'], $options['header'], $options['footer'], $options['container']); + + if (!isset($options['class'])) { + $options['class'] = $this->errorSummaryClass; + } + + if ($lines !== array()) { + $content = "
  • " . implode("
  • \n
  • ", ArrayHelper::htmlEncode($lines)) . "
    • "; + return Html::tag($container, $header . $content . $footer, $options); + } else { + $content = "
        "; + $options['style'] = isset($options['style']) ? rtrim($options['style'], ';') . '; display:none' : 'display:none'; + return Html::tag($container, $header . $content . $footer, $options); + } } + /** + * @param Model $model + * @param string $attribute + * @param array $options + * @return string + */ public function error($model, $attribute, $options = array()) { + self::resolveName($model, $attribute); // turn [a][b]attr into attr + $container = isset($options['container']) ? $options['container'] : 'div'; + unset($options['container']); + $error = $model->getFirstError($attribute); + return Html::tag($container, Html::encode($error), $options); + } + + public function resolveAttributeName($name) + { + } public function label($model, $attribute, $options = array()) From b5be2ccf18f0e504bad2eff6d5828c90f8c85487 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Fri, 15 Mar 2013 17:01:55 -0400 Subject: [PATCH 68/84] activeform WIP --- framework/widgets/ActiveForm.php | 134 ++++++++++++++++++++++++++++++++++----- 1 file changed, 119 insertions(+), 15 deletions(-) diff --git a/framework/widgets/ActiveForm.php b/framework/widgets/ActiveForm.php index 4a0f89c..2d47253 100644 --- a/framework/widgets/ActiveForm.php +++ b/framework/widgets/ActiveForm.php @@ -8,6 +8,7 @@ namespace yii\widgets; use Yii; +use yii\base\InvalidParamException; use yii\base\Widget; use yii\base\Model; use yii\util\Html; @@ -35,6 +36,7 @@ class ActiveForm extends Widget * @see errorSummary() */ public $errorSummaryClass = 'yii-error-summary'; + public $errorMessageClass = 'yii-error-message'; /** * @var string the default CSS class that indicates an input has error. * This is @@ -50,7 +52,10 @@ class ActiveForm extends Widget public $enableClientValidation = false; public $options = array(); - + /** + * @var array model-class mapped to name prefix + */ + public $modelMap; /** * @param Model|Model[] $models @@ -63,7 +68,7 @@ class ActiveForm extends Widget $models = array($models); } - $showAll = isset($options['showAll']) && $options['showAll']); + $showAll = isset($options['showAll']) && $options['showAll']; $lines = array(); /** @var $model Model */ foreach ($models as $model) { @@ -78,20 +83,22 @@ class ActiveForm extends Widget $header = isset($options['header']) ? $options['header'] : '

        ' . Yii::t('yii|Please fix the following errors:') . '

        '; $footer = isset($options['footer']) ? $options['footer'] : ''; - $container = isset($options['container']) ? $options['container'] : 'div'; + $tag = isset($options['tag']) ? $options['tag'] : 'div'; unset($options['showAll'], $options['header'], $options['footer'], $options['container']); if (!isset($options['class'])) { $options['class'] = $this->errorSummaryClass; + } else { + $options['class'] .= ' ' . $this->errorSummaryClass; } if ($lines !== array()) { $content = "
        • " . implode("
        • \n
        • ", ArrayHelper::htmlEncode($lines)) . "
          • "; - return Html::tag($container, $header . $content . $footer, $options); + return Html::tag($tag, $header . $content . $footer, $options); } else { $content = "
              "; $options['style'] = isset($options['style']) ? rtrim($options['style'], ';') . '; display:none' : 'display:none'; - return Html::tag($container, $header . $content . $footer, $options); + return Html::tag($tag, $header . $content . $footer, $options); } } @@ -103,25 +110,32 @@ class ActiveForm extends Widget */ public function error($model, $attribute, $options = array()) { - self::resolveName($model, $attribute); // turn [a][b]attr into attr - $container = isset($options['container']) ? $options['container'] : 'div'; - unset($options['container']); + $attribute = $this->normalizeAttributeName($attribute); + $this->getInputName($model, $attribute); + $tag = isset($options['tag']) ? $options['tag'] : 'div'; + unset($options['tag']); $error = $model->getFirstError($attribute); - return Html::tag($container, Html::encode($error), $options); - } - - public function resolveAttributeName($name) - { - + return Html::tag($tag, Html::encode($error), $options); } + /** + * @param Model $model + * @param string $attribute + * @param array $options + * @return string + */ public function label($model, $attribute, $options = array()) { + $attribute = $this->normalizeAttributeName($attribute); + $label = $model->getAttributeLabel($attribute); + return Html::label(Html::encode($label), isset($options['for']) ? $options['for'] : null, $options); } public function input($type, $model, $attribute, $options = array()) { - return ''; + $value = $this->getAttributeValue($model, $attribute); + $name = $this->getInputName($model, $attribute); + return Html::input($type, $name, $value, $options); } public function textInput($model, $attribute, $options = array()) @@ -146,29 +160,119 @@ class ActiveForm extends Widget public function textarea($model, $attribute, $options = array()) { + $value = $this->getAttributeValue($model, $attribute); + $name = $this->getInputName($model, $attribute); + return Html::textarea($name, $value, $options); } public function radio($model, $attribute, $value = '1', $options = array()) { + $checked = $this->getAttributeValue($model, $attribute); + $name = $this->getInputName($model, $attribute); + if (!array_key_exists('uncheck', $options)) { + $options['unchecked'] = '0'; + } + return Html::radio($name, $checked, $value, $options); } public function checkbox($model, $attribute, $value = '1', $options = array()) { + $checked = $this->getAttributeValue($model, $attribute); + $name = $this->getInputName($model, $attribute); + if (!array_key_exists('uncheck', $options)) { + $options['unchecked'] = '0'; + } + return Html::checkbox($name, $checked, $value, $options); } public function dropDownList($model, $attribute, $items, $options = array()) { + $checked = $this->getAttributeValue($model, $attribute); + $name = $this->getInputName($model, $attribute); + return Html::dropDownList($name, $checked, $items, $options); } public function listBox($model, $attribute, $items, $options = array()) { + $checked = $this->getAttributeValue($model, $attribute); + $name = $this->getInputName($model, $attribute); + if (!array_key_exists('unselect', $options)) { + $options['unselect'] = '0'; + } + return Html::listBox($name, $checked, $items, $options); } public function checkboxList($model, $attribute, $items, $options = array()) { + $checked = $this->getAttributeValue($model, $attribute); + $name = $this->getInputName($model, $attribute); + if (!array_key_exists('unselect', $options)) { + $options['unselect'] = '0'; + } + return Html::checkboxList($name, $checked, $items, $options); } public function radioList($model, $attribute, $items, $options = array()) { + $checked = $this->getAttributeValue($model, $attribute); + $name = $this->getInputName($model, $attribute); + if (!array_key_exists('unselect', $options)) { + $options['unselect'] = '0'; + } + return Html::radioList($name, $checked, $items, $options); + } + + public function getInputName($model, $attribute) + { + $class = get_class($model); + if (isset($this->modelMap[$class])) { + $class = $this->modelMap[$class]; + } elseif (($pos = strrpos($class, '\\')) !== false) { + $class = substr($class, $pos); + } + if (!preg_match('/(^|.*\])(\w+)(\[.*|$)/', $attribute, $matches)) { + throw new InvalidParamException('Attribute name must contain word characters only.'); + } + $prefix = $matches[1]; + $attribute = $matches[2]; + $suffix = $matches[3]; + if ($class === '' && $prefix === '') { + return $attribute . $suffix; + } elseif ($class !== '') { + return $class . $prefix . "[$attribute]" . $suffix; + } else { + throw new InvalidParamException('Model name cannot be mapped to empty for tabular inputs.'); + } + } + + public function getAttributeValue($model, $attribute) + { + if (!preg_match('/(^|.*\])(\w+)(\[.*|$)/', $attribute, $matches)) { + throw new InvalidParamException('Attribute name must contain word characters only.'); + } + $attribute = $matches[2]; + $index = $matches[3]; + if ($index === '') { + return $model->$attribute; + } else { + $value = $model->$attribute; + foreach (explode('][', trim($index, '[]')) as $id) { + if ((is_array($value) || $value instanceof \ArrayAccess) && isset($value[$id])) { + $value = $value[$id]; + } else { + return null; + } + } + return $value; + } + } + + public function normalizeAttributeName($attribute) + { + if (preg_match('/(^|.*\])(\w+)(\[.*|$)/', $attribute, $matches)) { + return $matches[2]; + } else { + throw new InvalidParamException('Attribute name must contain word characters only.'); + } } } From b2b9b4f319551f31d1c41d43fa93635d4f8ac298 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Sat, 16 Mar 2013 10:18:44 -0400 Subject: [PATCH 69/84] test wip --- framework/test/TestCase.php | 4 ++-- framework/test/WebTestCase.php | 25 +++++++++++++++++++++++++ framework/widgets/ActiveForm.php | 2 +- 3 files changed, 28 insertions(+), 3 deletions(-) create mode 100644 framework/test/WebTestCase.php diff --git a/framework/test/TestCase.php b/framework/test/TestCase.php index 3fd92c0..f190e5a 100644 --- a/framework/test/TestCase.php +++ b/framework/test/TestCase.php @@ -10,9 +10,9 @@ namespace yii\test; require_once('PHPUnit/Runner/Version.php'); -spl_autoload_unregister(array('YiiBase','autoload')); +spl_autoload_unregister(array('Yii','autoload')); require_once('PHPUnit/Autoload.php'); -spl_autoload_register(array('YiiBase','autoload')); // put yii's autoloader at the end +spl_autoload_register(array('Yii','autoload')); // put yii's autoloader at the end /** * TestCase is the base class for all test case classes. diff --git a/framework/test/WebTestCase.php b/framework/test/WebTestCase.php new file mode 100644 index 0000000..39162c9 --- /dev/null +++ b/framework/test/WebTestCase.php @@ -0,0 +1,25 @@ + + * @since 2.0 + */ +abstract class WebTestCase extends \PHPUnit_Extensions_SeleniumTestCase +{ +} diff --git a/framework/widgets/ActiveForm.php b/framework/widgets/ActiveForm.php index 4a0f89c..b8c8f2c 100644 --- a/framework/widgets/ActiveForm.php +++ b/framework/widgets/ActiveForm.php @@ -63,7 +63,7 @@ class ActiveForm extends Widget $models = array($models); } - $showAll = isset($options['showAll']) && $options['showAll']); + $showAll = isset($options['showAll']) && $options['showAll']; $lines = array(); /** @var $model Model */ foreach ($models as $model) { From 3b610926f4eb569ea3992f5ed2a8b3e65b615a01 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Sat, 16 Mar 2013 10:21:15 -0400 Subject: [PATCH 70/84] Added web test folders. --- tests/web/app/assets/.gitignore | 1 + tests/web/app/protected/runtime/.gitignore | 1 + 2 files changed, 2 insertions(+) create mode 100644 tests/web/app/assets/.gitignore create mode 100644 tests/web/app/protected/runtime/.gitignore diff --git a/tests/web/app/assets/.gitignore b/tests/web/app/assets/.gitignore new file mode 100644 index 0000000..72e8ffc --- /dev/null +++ b/tests/web/app/assets/.gitignore @@ -0,0 +1 @@ +* diff --git a/tests/web/app/protected/runtime/.gitignore b/tests/web/app/protected/runtime/.gitignore new file mode 100644 index 0000000..72e8ffc --- /dev/null +++ b/tests/web/app/protected/runtime/.gitignore @@ -0,0 +1 @@ +* From 9a4f4f85d26ad796bdb6440fd895240f860a2e8d Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Mon, 18 Mar 2013 08:42:12 -0400 Subject: [PATCH 71/84] wip --- tests/web/app/index.php | 6 +++++ tests/web/app/protected/config/main.php | 3 +++ .../app/protected/controllers/SiteController.php | 30 ++++++++++++++++++++++ tests/web/app/protected/views/site/index.php | 8 ++++++ 4 files changed, 47 insertions(+) create mode 100644 tests/web/app/index.php create mode 100644 tests/web/app/protected/config/main.php create mode 100644 tests/web/app/protected/controllers/SiteController.php create mode 100644 tests/web/app/protected/views/site/index.php diff --git a/tests/web/app/index.php b/tests/web/app/index.php new file mode 100644 index 0000000..4cfa1ab --- /dev/null +++ b/tests/web/app/index.php @@ -0,0 +1,6 @@ +run(); diff --git a/tests/web/app/protected/config/main.php b/tests/web/app/protected/config/main.php new file mode 100644 index 0000000..eed6d54 --- /dev/null +++ b/tests/web/app/protected/config/main.php @@ -0,0 +1,3 @@ + 'item 1', + 'value 2' => 'item 2', + 'value 3' => 'item 3', + ), isset($_POST['test']) ? $_POST['test'] : null, + function ($index, $label, $name, $value, $checked) { + return Html::label( + $label . ' ' . Html::checkbox($name, $value, $checked), + null, array('class' => 'inline checkbox') + ); + }); + echo Html::submitButton(); + echo Html::endForm(); + print_r($_POST); + } +} \ No newline at end of file diff --git a/tests/web/app/protected/views/site/index.php b/tests/web/app/protected/views/site/index.php new file mode 100644 index 0000000..5decb56 --- /dev/null +++ b/tests/web/app/protected/views/site/index.php @@ -0,0 +1,8 @@ + Date: Wed, 20 Mar 2013 22:03:13 -0400 Subject: [PATCH 72/84] refactored View. --- framework/YiiBase.php | 46 +- framework/base/Application.php | 44 +- framework/base/Controller.php | 144 +++++- framework/base/ErrorHandler.php | 9 +- framework/base/Theme.php | 9 +- framework/base/View.php | 509 +++++++-------------- framework/base/Widget.php | 77 +++- framework/caching/FileCache.php | 3 - .../console/controllers/MigrateController.php | 2 +- framework/util/FileHelper.php | 2 +- framework/views/error.php | 10 +- framework/views/exception.php | 16 +- framework/web/Session.php | 2 +- 13 files changed, 419 insertions(+), 454 deletions(-) diff --git a/framework/YiiBase.php b/framework/YiiBase.php index 392dd1c..678856c 100644 --- a/framework/YiiBase.php +++ b/framework/YiiBase.php @@ -4,9 +4,9 @@ * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ - use yii\base\Exception; use yii\base\InvalidConfigException; +use yii\base\InvalidParamException; use yii\logging\Logger; /** @@ -94,7 +94,7 @@ class YiiBase */ public static $objectConfig = array(); - private static $_imported = array(); // alias => class name or directory + private static $_imported = array(); // alias => class name or directory private static $_logger; /** @@ -159,9 +159,7 @@ class YiiBase return self::$_imported[$alias] = $className; } - if (($path = static::getAlias(dirname($alias))) === false) { - throw new Exception('Invalid path alias: ' . $alias); - } + $path = static::getAlias(dirname($alias)); if ($isClass) { if ($forceInclude) { @@ -191,24 +189,30 @@ class YiiBase * * Note, this method does not ensure the existence of the resulting path. * @param string $alias alias + * @param boolean $throwException whether to throw an exception if the given alias is invalid. + * If this is false and an invalid alias is given, false will be returned by this method. * @return string|boolean path corresponding to the alias, false if the root alias is not previously registered. * @see setAlias */ - public static function getAlias($alias) + public static function getAlias($alias, $throwException = true) { - if (!is_string($alias)) { - return false; - } elseif (isset(self::$aliases[$alias])) { - return self::$aliases[$alias]; - } elseif ($alias === '' || $alias[0] !== '@') { // not an alias - return $alias; - } elseif (($pos = strpos($alias, '/')) !== false) { - $rootAlias = substr($alias, 0, $pos); - if (isset(self::$aliases[$rootAlias])) { - return self::$aliases[$alias] = self::$aliases[$rootAlias] . substr($alias, $pos); + if (is_string($alias)) { + if (isset(self::$aliases[$alias])) { + return self::$aliases[$alias]; + } elseif ($alias === '' || $alias[0] !== '@') { // not an alias + return $alias; + } elseif (($pos = strpos($alias, '/')) !== false || ($pos = strpos($alias, '\\')) !== false) { + $rootAlias = substr($alias, 0, $pos); + if (isset(self::$aliases[$rootAlias])) { + return self::$aliases[$alias] = self::$aliases[$rootAlias] . substr($alias, $pos); + } } } - return false; + if ($throwException) { + throw new InvalidParamException("Invalid path alias: $alias"); + } else { + return false; + } } /** @@ -236,10 +240,8 @@ class YiiBase unset(self::$aliases[$alias]); } elseif ($path[0] !== '@') { self::$aliases[$alias] = rtrim($path, '\\/'); - } elseif (($p = static::getAlias($path)) !== false) { - self::$aliases[$alias] = $p; } else { - throw new Exception('Invalid path: ' . $path); + self::$aliases[$alias] = static::getAlias($path); } } @@ -273,14 +275,14 @@ class YiiBase // namespaced class, e.g. yii\base\Component // convert namespace to path alias, e.g. yii\base\Component to @yii/base/Component $alias = '@' . str_replace('\\', '/', ltrim($className, '\\')); - if (($path = static::getAlias($alias)) !== false) { + if (($path = static::getAlias($alias, false)) !== false) { $classFile = $path . '.php'; } } elseif (($pos = strpos($className, '_')) !== false) { // PEAR-styled class, e.g. PHPUnit_Framework_TestCase // convert class name to path alias, e.g. PHPUnit_Framework_TestCase to @PHPUnit/Framework/TestCase $alias = '@' . str_replace('_', '/', $className); - if (($path = static::getAlias($alias)) !== false) { + if (($path = static::getAlias($alias, false)) !== false) { $classFile = $path . '.php'; } } diff --git a/framework/base/Application.php b/framework/base/Application.php index a60bf90..bbc4601 100644 --- a/framework/base/Application.php +++ b/framework/base/Application.php @@ -160,28 +160,28 @@ class Application extends Module */ public function handleFatalError() { - if(YII_ENABLE_ERROR_HANDLER) { + if (YII_ENABLE_ERROR_HANDLER) { $error = error_get_last(); - if(ErrorException::isFatalErorr($error)) { + if (ErrorException::isFatalErorr($error)) { unset($this->_memoryReserve); $exception = new ErrorException($error['message'], $error['type'], $error['type'], $error['file'], $error['line']); - if(function_exists('xdebug_get_function_stack')) { + if (function_exists('xdebug_get_function_stack')) { $trace = array_slice(array_reverse(xdebug_get_function_stack()), 4, -1); - foreach($trace as &$frame) { - if(!isset($frame['function'])) { + foreach ($trace as &$frame) { + if (!isset($frame['function'])) { $frame['function'] = 'unknown'; } // XDebug < 2.1.1: http://bugs.xdebug.org/view.php?id=695 - if(!isset($frame['type'])) { + if (!isset($frame['type'])) { $frame['type'] = '::'; } // XDebug has a different key name $frame['args'] = array(); - if(isset($frame['params']) && !isset($frame['args'])) { + if (isset($frame['params']) && !isset($frame['args'])) { $frame['args'] = $frame['params']; } } @@ -214,8 +214,8 @@ class Application extends Module $this->beforeRequest(); // Allocating twice more than required to display memory exhausted error // in case of trying to allocate last 1 byte while all memory is taken. - $this->_memoryReserve = str_repeat('x', 1024*256); - register_shutdown_function(array($this,'end'),0,false); + $this->_memoryReserve = str_repeat('x', 1024 * 256); + register_shutdown_function(array($this, 'end'), 0, false); $status = $this->processRequest(); $this->afterRequest(); return $status; @@ -346,15 +346,6 @@ class Application extends Module } /** - * Returns the application theme. - * @return Theme the theme that this application is currently using. - */ - public function getTheme() - { - return $this->getComponent('theme'); - } - - /** * Returns the cache component. * @return \yii\caching\Cache the cache application component. Null if the component is not enabled. */ @@ -373,12 +364,12 @@ class Application extends Module } /** - * Returns the view renderer. - * @return ViewRenderer the view renderer used by this application. + * Returns the view object. + * @return View the view object that is used to render various view files. */ - public function getViewRenderer() + public function getView() { - return $this->getComponent('viewRenderer'); + return $this->getComponent('view'); } /** @@ -423,6 +414,9 @@ class Application extends Module 'urlManager' => array( 'class' => 'yii\web\UrlManager', ), + 'view' => array( + 'class' => 'yii\base\View', + ), )); } @@ -446,8 +440,8 @@ class Application extends Module // in case error appeared in __toString method we can't throw any exception $trace = debug_backtrace(false); array_shift($trace); - foreach($trace as $frame) { - if($frame['function'] == '__toString') { + foreach ($trace as $frame) { + if ($frame['function'] == '__toString') { $this->handleException($exception); } } @@ -481,7 +475,7 @@ class Application extends Module $this->end(1); - } catch(\Exception $e) { + } catch (\Exception $e) { // exception could be thrown in end() or ErrorHandler::handle() $msg = (string)$e; $msg .= "\nPrevious exception:\n"; diff --git a/framework/base/Controller.php b/framework/base/Controller.php index 65200bb..8840cca 100644 --- a/framework/base/Controller.php +++ b/framework/base/Controller.php @@ -8,6 +8,7 @@ namespace yii\base; use Yii; +use yii\util\FileHelper; use yii\util\StringHelper; /** @@ -295,34 +296,42 @@ class Controller extends Component /** * Renders a view and applies layout if available. - * - * @param $view - * @param array $params - * @return string + * @param string $view the view name. Please refer to [[findViewFile()]] on how to specify a view name. + * @param array $params the parameters (name-value pairs) that should be made available in the view. + * These parameters will not be available in the layout. + * @return string the rendering result. + * @throws InvalidParamException if the view file or the layout file does not exist. */ public function render($view, $params = array()) { - return $this->createView()->render($view, $params); - } - - public function renderContent($content) - { - return $this->createView()->renderContent($content); + $viewFile = $this->findViewFile($view); + $layoutFile = $this->findLayoutFile(); + return Yii::$app->getView()->render($this, $viewFile, $params, $layoutFile); } + /** + * Renders a view. + * This method differs from [[render()]] in that it does not apply any layout. + * @param string $view the view name. Please refer to [[findViewFile()]] on how to specify a view name. + * @param array $params the parameters (name-value pairs) that should be made available in the view. + * @return string the rendering result. + * @throws InvalidParamException if the view file does not exist. + */ public function renderPartial($view, $params = array()) { - return $this->createView()->renderPartial($view, $params); + return $this->renderFile($this->findViewFile($view), $params); } + /** + * Renders a view file. + * @param string $file the view file to be rendered. This can be either a file path or a path alias. + * @param array $params the parameters (name-value pairs) that should be made available in the view. + * @return string the rendering result. + * @throws InvalidParamException if the view file does not exist. + */ public function renderFile($file, $params = array()) { - return $this->createView()->renderFile($file, $params); - } - - public function createView() - { - return new View($this); + return Yii::$app->getView()->render($this, $file, $params); } /** @@ -335,4 +344,105 @@ class Controller extends Component { return $this->module->getViewPath() . DIRECTORY_SEPARATOR . $this->id; } + + /** + * Finds the view file based on the given view name. + * + * A view name can be specified in one of the following formats: + * + * - path alias (e.g. "@app/views/site/index"); + * - absolute path within application (e.g. "//site/index"): the view name starts with double slashes. + * The actual view file will be looked for under the [[Application::viewPath|view path]] of the application. + * - absolute path within module (e.g. "/site/index"): the view name starts with a single slash. + * The actual view file will be looked for under the [[Module::viewPath|view path]] of the currently + * active module. + * - relative path (e.g. "index"): the actual view file will be looked for under [[viewPath]]. + * + * If the view name does not contain a file extension, it will use the default one `.php`. + * + * @param string $view the view name or the path alias of the view file. + * @return string the view file path. Note that the file may not exist. + * @throws InvalidParamException if the view file is an invalid path alias + */ + protected function findViewFile($view) + { + if (strncmp($view, '@', 1) === 0) { + // e.g. "@app/views/common" + $file = Yii::getAlias($view); + } elseif (strncmp($view, '/', 1) !== 0) { + // e.g. "index" + $file = $this->getViewPath() . DIRECTORY_SEPARATOR . $view; + } elseif (strncmp($view, '//', 2) !== 0) { + // e.g. "/site/index" + $file = $this->module->getViewPath() . DIRECTORY_SEPARATOR . ltrim($view, '/'); + } else { + // e.g. "//layouts/main" + $file = Yii::$app->getViewPath() . DIRECTORY_SEPARATOR . ltrim($view, '/'); + } + if (FileHelper::getExtension($file) === '') { + $file .= '.php'; + } + return $file; + } + + /** + * Finds the applicable layout file. + * + * This method locates an applicable layout file via two steps. + * + * In the first step, it determines the layout name and the context module: + * + * - If [[layout]] is specified as a string, use it as the layout name and [[module]] as the context module; + * - If [[layout]] is null, search through all ancestor modules of this controller and find the first + * module whose [[Module::layout|layout]] is not null. The layout and the corresponding module + * are used as the layout name and the context module, respectively. If such a module is not found + * or the corresponding layout is not a string, it will return false, meaning no applicable layout. + * + * In the second step, it determines the actual layout file according to the previously found layout name + * and context module. The layout name can be + * + * - a path alias (e.g. "@app/views/layouts/main"); + * - an absolute path (e.g. "/main"): the layout name starts with a slash. The actual layout file will be + * looked for under the [[Application::layoutPath|layout path]] of the application; + * - a relative path (e.g. "main"): the actual layout layout file will be looked for under the + * [[Module::viewPath|view path]] of the context module. + * + * If the layout name does not contain a file extension, it will use the default one `.php`. + * + * @return string|boolean the layout file path, or false if layout is not needed. + * @throws InvalidParamException if an invalid path alias is used to specify the layout + */ + protected function findLayoutFile() + { + /** @var $module Module */ + if (is_string($this->layout)) { + $module = $this->module; + $view = $this->layout; + } elseif ($this->layout === null) { + $module = $this->module; + while ($module !== null && $module->layout === null) { + $module = $module->module; + } + if ($module !== null && is_string($module->layout)) { + $view = $module->layout; + } + } + + if (!isset($view)) { + return false; + } + + if (strncmp($view, '@', 1) === 0) { + $file = Yii::getAlias($view); + } elseif (strncmp($view, '/', 1) === 0) { + $file = Yii::$app->getLayoutPath() . DIRECTORY_SEPARATOR . $view; + } else { + $file = $module->getLayoutPath() . DIRECTORY_SEPARATOR . $view; + } + + if (FileHelper::getExtension($file) === '') { + $file .= '.php'; + } + return $file; + } } diff --git a/framework/base/ErrorHandler.php b/framework/base/ErrorHandler.php index 40ae6d8..996fa18 100644 --- a/framework/base/ErrorHandler.php +++ b/framework/base/ErrorHandler.php @@ -253,14 +253,9 @@ class ErrorHandler extends Component */ public function renderAsHtml($exception) { - $view = new View($this); - if (!YII_DEBUG || $exception instanceof UserException) { - $viewName = $this->errorView; - } else { - $viewName = $this->exceptionView; - } + $view = new View; $name = !YII_DEBUG || $exception instanceof HttpException ? $this->errorView : $this->exceptionView; - echo $view->render($name, array( + echo $view->render($this, $name, array( 'exception' => $exception, )); } diff --git a/framework/base/Theme.php b/framework/base/Theme.php index 61487d8..c5fd925 100644 --- a/framework/base/Theme.php +++ b/framework/base/Theme.php @@ -40,7 +40,8 @@ class Theme extends Component /** * @var array the mapping between view directories and their corresponding themed versions. * If not set, it will be initialized as a mapping from [[Application::basePath]] to [[basePath]]. - * This property is used by [[apply()]] when a view is trying to apply the theme. + * This property is used by [[applyTo()]] when a view is trying to apply the theme. + * Path aliases can be used when specifying directories. */ public $pathMap; @@ -63,7 +64,9 @@ class Theme extends Component } $paths = array(); foreach ($this->pathMap as $from => $to) { - $paths[FileHelper::normalizePath($from) . DIRECTORY_SEPARATOR] = FileHelper::normalizePath($to) . DIRECTORY_SEPARATOR; + $from = FileHelper::normalizePath(Yii::getAlias($from)); + $to = FileHelper::normalizePath(Yii::getAlias($to)); + $paths[$from . DIRECTORY_SEPARATOR] = $to . DIRECTORY_SEPARATOR; } $this->pathMap = $paths; } @@ -93,7 +96,7 @@ class Theme extends Component * @param string $path the file to be themed * @return string the themed file, or the original file if the themed version is not available. */ - public function apply($path) + public function applyTo($path) { $path = FileHelper::normalizePath($path); foreach ($this->pathMap as $from => $to) { diff --git a/framework/base/View.php b/framework/base/View.php index 2b2e7d1..75a911d 100644 --- a/framework/base/View.php +++ b/framework/base/View.php @@ -8,14 +8,14 @@ namespace yii\base; use Yii; -use yii\util\FileHelper; use yii\base\Application; +use yii\util\FileHelper; /** * View represents a view object in the MVC pattern. - * + * * View provides a set of methods (e.g. [[render()]]) for rendering purpose. - * + * * @author Qiang Xue * @since 2.0 */ @@ -24,133 +24,112 @@ class View extends Component /** * @var object the object that owns this view. This can be a controller, a widget, or any other object. */ - public $owner; - /** - * @var string the layout to be applied when [[render()]] or [[renderContent()]] is called. - * If not set, it will use the [[Module::layout]] of the currently active module. - */ - public $layout; - /** - * @var string the language that the view should be rendered in. If not set, it will use - * the value of [[Application::language]]. - */ - public $language; + public $context; /** - * @var string the language that the original view is in. If not set, it will use - * the value of [[Application::sourceLanguage]]. + * @var mixed custom parameters that are shared among view templates. */ - public $sourceLanguage; - /** - * @var boolean whether to localize the view when possible. Defaults to true. - * Note that when this is true, if a localized view cannot be found, the original view will be rendered. - * No error will be reported. - */ - public $enableI18N = true; + public $params; /** - * @var boolean whether to theme the view when possible. Defaults to true. - * Note that theming will be disabled if [[Application::theme]] is not set. + * @var ViewRenderer|array the view renderer object or the configuration array for + * creating the view renderer. If not set, view files will be treated as normal PHP files. */ - public $enableTheme = true; + public $renderer; /** - * @var mixed custom parameters that are available in the view template + * @var Theme|array the theme object or the configuration array for creating the theme. + * If not set, it means theming is not enabled. */ - public $params; + public $theme; /** * @var Widget[] the widgets that are currently not ended */ - private $_widgetStack = array(); - - /** - * Constructor. - * @param object $owner the owner of this view. This usually is a controller or a widget. - * @param array $config name-value pairs that will be used to initialize the object properties - */ - public function __construct($owner, $config = array()) - { - $this->owner = $owner; - parent::__construct($config); - } + private $_widgetStack = array(); - /** - * Renders a view within a layout. - * This method is similar to [[renderPartial()]] except that if a layout is available, - * this method will embed the view result into the layout and then return it. - * @param string $view the view to be rendered. Please refer to [[findViewFile()]] on possible formats of the view name. - * @param array $params the parameters that should be made available in the view. The PHP function `extract()` - * will be called on this variable to extract the variables from this parameter. - * @return string the rendering result - * @throws InvalidConfigException if the view file or layout file cannot be found - * @see findViewFile() - * @see findLayoutFile() - */ - public function render($view, $params = array()) - { - $content = $this->renderPartial($view, $params); - return $this->renderContent($content); - } /** - * Renders a text content within a layout. - * The layout being used is resolved by [[findLayout()]]. - * If no layout is available, the content will be returned back. - * @param string $content the content to be rendered - * @return string the rendering result - * @throws InvalidConfigException if the layout file cannot be found - * @see findLayoutFile() + * Initializes the view component. */ - public function renderContent($content) + public function init() { - $layoutFile = $this->findLayoutFile(); - if ($layoutFile !== false) { - return $this->renderFile($layoutFile, array('content' => $content)); - } else { - return $content; + parent::init(); + if (is_array($this->renderer)) { + $this->renderer = Yii::createObject($this->renderer); + } + if (is_array($this->theme)) { + $this->theme = Yii::createObject($this->theme); } } /** - * Renders a view. + * Renders a view file under a context with an optional layout. + * + * This method is similar to [[renderFile()]] except that it will update [[context]] + * with the provided $context parameter. It will also apply layout to the rendering result + * of the view file if $layoutFile is given. * - * The method first finds the actual view file corresponding to the specified view. - * It then calls [[renderFile()]] to render the view file. The rendering result is returned - * as a string. If the view file does not exist, an exception will be thrown. + * Theming and localization will be performed for the view file and the layout file, if possible. * - * @param string $view the view to be rendered. Please refer to [[findViewFile()]] on possible formats of the view name. - * @param array $params the parameters that should be made available in the view. The PHP function `extract()` - * will be called on this variable to extract the variables from this parameter. + * @param object $context the context object for rendering the file. This could be a controller, a widget, + * or any other object that serves as the rendering context of the view file. In the view file, + * it can be accessed through the [[context]] property. + * @param string $viewFile the view file. This can be a file path or a path alias. + * @param array $params the parameters (name-value pairs) that will be extracted and made available in the view file. + * @param string|boolean $layoutFile the layout file. This can be a file path or a path alias. + * If it is false, it means no layout should be applied. * @return string the rendering result - * @throws InvalidParamException if the view file cannot be found - * @see findViewFile() + * @throws InvalidParamException if the view file or the layout file does not exist. */ - public function renderPartial($view, $params = array()) + public function render($context, $viewFile, $params = array(), $layoutFile = false) { - $file = $this->findViewFile($view); - if ($file !== false) { - return $this->renderFile($file, $params); - } else { - throw new InvalidParamException("Unable to find the view file for view '$view'."); + $oldContext = $this->context; + $this->context = $context; + + $content = $this->renderFile($viewFile, $params); + + if ($layoutFile !== false) { + $content = $this->renderFile($layoutFile, array('content' => $content)); } + + $this->context = $oldContext; + + return $content; } /** * Renders a view file. * - * If a [[ViewRenderer|view renderer]] is installed, this method will try to use the view renderer - * to render the view file. Otherwise, it will simply include the view file, capture its output - * and return it as a string. + * This method renders the specified view file under the existing [[context]]. + * + * If [[theme]] is enabled (not null), it will try to render the themed version of the view file as long + * as it is available. * - * @param string $file the view file. + * The method will call [[FileHelper::localize()]] to localize the view file. + * + * If [[renderer]] is enabled (not null), the method will use it to render the view file. + * Otherwise, it will simply include the view file as a normal PHP file, capture its output and + * return it as a string. + * + * @param string $viewFile the view file. This can be a file path or a path alias. * @param array $params the parameters (name-value pairs) that will be extracted and made available in the view file. * @return string the rendering result + * @throws InvalidParamException if the view file does not exist */ - public function renderFile($file, $params = array()) + public function renderFile($viewFile, $params = array()) { - $renderer = Yii::$app->getViewRenderer(); - if ($renderer !== null) { - return $renderer->render($this, $file, $params); + $viewFile = Yii::getAlias($viewFile); + if (is_file($viewFile)) { + if ($this->theme !== null) { + $viewFile = $this->theme->applyTo($viewFile); + } + $viewFile = FileHelper::localize($viewFile); + } else { + throw new InvalidParamException("The view file does not exist: $viewFile"); + } + + if ($this->renderer !== null) { + return $this->renderer->render($this, $viewFile, $params); } else { - return $this->renderPhpFile($file, $params); + return $this->renderPhpFile($viewFile, $params); } } @@ -161,6 +140,8 @@ class View extends Component * It extracts the given parameters and makes them available in the view file. * The method captures the output of the included view file and returns it as a string. * + * This method should mainly be called by view renderer or [[renderFile()]]. + * * @param string $_file_ the view file. * @param array $_params_ the parameters (name-value pairs) that will be extracted and made available in the view file. * @return string the rendering result @@ -184,7 +165,7 @@ class View extends Component public function createWidget($class, $properties = array()) { $properties['class'] = $class; - return Yii::createObject($properties, $this->owner); + return Yii::createObject($properties, $this->context); } /** @@ -233,7 +214,7 @@ class View extends Component * If you want to capture the rendering result of a widget, you may use * [[createWidget()]] and [[Widget::run()]]. * @return Widget the widget instance - * @throws Exception if [[beginWidget()]] and [[endWidget()]] calls are not properly nested + * @throws InvalidCallException if [[beginWidget()]] and [[endWidget()]] calls are not properly nested */ public function endWidget() { @@ -242,251 +223,99 @@ class View extends Component $widget->run(); return $widget; } else { - throw new Exception("Unmatched beginWidget() and endWidget() calls."); - } - } -// -// /** -// * Begins recording a clip. -// * This method is a shortcut to beginning [[yii\widgets\Clip]] -// * @param string $id the clip ID. -// * @param array $properties initial property values for [[yii\widgets\Clip]] -// */ -// public function beginClip($id, $properties = array()) -// { -// $properties['id'] = $id; -// $this->beginWidget('yii\widgets\Clip', $properties); -// } -// -// /** -// * Ends recording a clip. -// */ -// public function endClip() -// { -// $this->endWidget(); -// } -// -// /** -// * Begins fragment caching. -// * This method will display cached content if it is available. -// * If not, it will start caching and would expect an [[endCache()]] -// * call to end the cache and save the content into cache. -// * A typical usage of fragment caching is as follows, -// * -// * ~~~ -// * if($this->beginCache($id)) { -// * // ...generate content here -// * $this->endCache(); -// * } -// * ~~~ -// * -// * @param string $id a unique ID identifying the fragment to be cached. -// * @param array $properties initial property values for [[yii\widgets\OutputCache]] -// * @return boolean whether we need to generate content for caching. False if cached version is available. -// * @see endCache -// */ -// public function beginCache($id, $properties = array()) -// { -// $properties['id'] = $id; -// $cache = $this->beginWidget('yii\widgets\OutputCache', $properties); -// if ($cache->getIsContentCached()) { -// $this->endCache(); -// return false; -// } else { -// return true; -// } -// } -// -// /** -// * Ends fragment caching. -// * This is an alias to [[endWidget()]] -// * @see beginCache -// */ -// public function endCache() -// { -// $this->endWidget(); -// } -// -// /** -// * Begins the rendering of content that is to be decorated by the specified view. -// * @param mixed $view the name of the view that will be used to decorate the content. The actual view script -// * is resolved via {@link getViewFile}. If this parameter is null (default), -// * the default layout will be used as the decorative view. -// * Note that if the current controller does not belong to -// * any module, the default layout refers to the application's {@link CWebApplication::layout default layout}; -// * If the controller belongs to a module, the default layout refers to the module's -// * {@link CWebModule::layout default layout}. -// * @param array $params the variables (name=>value) to be extracted and made available in the decorative view. -// * @see endContent -// * @see yii\widgets\ContentDecorator -// */ -// public function beginContent($view, $params = array()) -// { -// $this->beginWidget('yii\widgets\ContentDecorator', array( -// 'view' => $view, -// 'params' => $params, -// )); -// } -// -// /** -// * Ends the rendering of content. -// * @see beginContent -// */ -// public function endContent() -// { -// $this->endWidget(); -// } - - /** - * Finds the view file based on the given view name. - * - * A view name can be specified in one of the following formats: - * - * - path alias (e.g. "@app/views/site/index"); - * - absolute path within application (e.g. "//site/index"): the view name starts with double slashes. - * The actual view file will be looked for under the [[Application::viewPath|view path]] of the application. - * - absolute path within module (e.g. "/site/index"): the view name starts with a single slash. - * The actual view file will be looked for under the [[Module::viewPath|view path]] of the currently - * active module. - * - relative path (e.g. "index"): the actual view file will be looked for under the [[owner]]'s view path. - * If [[owner]] is a widget or a controller, its view path is given by their `viewPath` property. - * If [[owner]] is an object of any other type, its view path is the `view` sub-directory of the directory - * containing the owner class file. - * - * If the view name does not contain a file extension, it will default to `.php`. - * - * If [[enableTheme]] is true and there is an active application them, the method will also - * attempt to use a themed version of the view file, when available. - * - * And if [[enableI18N]] is true, the method will attempt to use a translated version of the view file, - * when available. - * - * @param string $view the view name or path alias. If the view name does not specify - * the view file extension name, it will use `.php` as the extension name. - * @return string the view file path if it exists. False if the view file cannot be found. - * @throws InvalidConfigException if the view file does not exist - */ - public function findViewFile($view) - { - if (FileHelper::getExtension($view) === '') { - $view .= '.php'; - } - if (strncmp($view, '@', 1) === 0) { - // e.g. "@app/views/common" - if (($file = Yii::getAlias($view)) === false) { - throw new InvalidConfigException("Invalid path alias: $view"); - } - } elseif (strncmp($view, '/', 1) !== 0) { - // e.g. "index" - if ($this->owner instanceof Controller || $this->owner instanceof Widget) { - $file = $this->owner->getViewPath() . DIRECTORY_SEPARATOR . $view; - } elseif ($this->owner !== null) { - $class = new \ReflectionClass($this->owner); - $file = dirname($class->getFileName()) . DIRECTORY_SEPARATOR . 'views' . DIRECTORY_SEPARATOR . $view; - } else { - $file = Yii::$app->getViewPath() . DIRECTORY_SEPARATOR . $view; - } - } elseif (strncmp($view, '//', 2) !== 0 && Yii::$app->controller !== null) { - // e.g. "/site/index" - $file = Yii::$app->controller->module->getViewPath() . DIRECTORY_SEPARATOR . ltrim($view, '/'); - } else { - // e.g. "//layouts/main" - $file = Yii::$app->getViewPath() . DIRECTORY_SEPARATOR . ltrim($view, '/'); - } - - if (is_file($file)) { - if ($this->enableTheme && ($theme = Yii::$app->getTheme()) !== null) { - $file = $theme->apply($file); - } - return $this->enableI18N ? FileHelper::localize($file, $this->language, $this->sourceLanguage) : $file; - } else { - throw new InvalidConfigException("View file for view '$view' does not exist: $file"); + throw new InvalidCallException("Unmatched beginWidget() and endWidget() calls."); } } - /** - * Finds the layout file that can be applied to the view. - * - * The applicable layout is resolved according to the following rules: - * - * - If [[layout]] is specified as a string, use it as the layout name and search for the layout file - * under the layout path of the currently active module; - * - If [[layout]] is null and [[owner]] is a controller: - * * If the controller's [[Controller::layout|layout]] is a string, use it as the layout name - * and search for the layout file under the layout path of the parent module of the controller; - * * If the controller's [[Controller::layout|layout]] is null, look through its ancestor modules - * and find the first one whose [[Module::layout|layout]] is not null. Use the layout specified - * by that module; - * - Returns false for all other cases. - * - * Like view names, a layout name can take several formats: - * - * - path alias (e.g. "@app/views/layouts/main"); - * - absolute path (e.g. "/main"): the layout name starts with a slash. The actual layout file will be - * looked for under the [[Application::layoutPath|layout path]] of the application; - * - relative path (e.g. "main"): the actual layout layout file will be looked for under the - * [[Module::viewPath|view path]] of the context module determined by the above layout resolution process. - * - * If the layout name does not contain a file extension, it will default to `.php`. - * - * If [[enableTheme]] is true and there is an active application them, the method will also - * attempt to use a themed version of the layout file, when available. - * - * And if [[enableI18N]] is true, the method will attempt to use a translated version of the layout file, - * when available. - * - * @return string|boolean the layout file path, or false if layout is not needed. - * @throws InvalidConfigException if the layout file cannot be found - */ - public function findLayoutFile() - { - /** @var $module Module */ - if (is_string($this->layout)) { - if (Yii::$app->controller) { - $module = Yii::$app->controller->module; - } else { - $module = Yii::$app; - } - $view = $this->layout; - } elseif ($this->owner instanceof Controller) { - if (is_string($this->owner->layout)) { - $module = $this->owner->module; - $view = $this->owner->layout; - } elseif ($this->owner->layout === null) { - $module = $this->owner->module; - while ($module !== null && $module->layout === null) { - $module = $module->module; - } - if ($module !== null && is_string($module->layout)) { - $view = $module->layout; - } - } - } - - if (!isset($view)) { - return false; - } - - if (FileHelper::getExtension($view) === '') { - $view .= '.php'; - } - if (strncmp($view, '@', 1) === 0) { - if (($file = Yii::getAlias($view)) === false) { - throw new InvalidConfigException("Invalid path alias: $view"); - } - } elseif (strncmp($view, '/', 1) === 0) { - $file = Yii::$app->getLayoutPath() . DIRECTORY_SEPARATOR . $view; - } else { - $file = $module->getLayoutPath() . DIRECTORY_SEPARATOR . $view; - } - - if (is_file($file)) { - if ($this->enableTheme && ($theme = Yii::$app->getTheme()) !== null) { - $file = $theme->apply($file); - } - return $this->enableI18N ? FileHelper::localize($file, $this->language, $this->sourceLanguage) : $file; - } else { - throw new InvalidConfigException("Layout file for layout '$view' does not exist: $file"); - } - } + // + // /** + // * Begins recording a clip. + // * This method is a shortcut to beginning [[yii\widgets\Clip]] + // * @param string $id the clip ID. + // * @param array $properties initial property values for [[yii\widgets\Clip]] + // */ + // public function beginClip($id, $properties = array()) + // { + // $properties['id'] = $id; + // $this->beginWidget('yii\widgets\Clip', $properties); + // } + // + // /** + // * Ends recording a clip. + // */ + // public function endClip() + // { + // $this->endWidget(); + // } + // + // /** + // * Begins fragment caching. + // * This method will display cached content if it is available. + // * If not, it will start caching and would expect an [[endCache()]] + // * call to end the cache and save the content into cache. + // * A typical usage of fragment caching is as follows, + // * + // * ~~~ + // * if($this->beginCache($id)) { + // * // ...generate content here + // * $this->endCache(); + // * } + // * ~~~ + // * + // * @param string $id a unique ID identifying the fragment to be cached. + // * @param array $properties initial property values for [[yii\widgets\OutputCache]] + // * @return boolean whether we need to generate content for caching. False if cached version is available. + // * @see endCache + // */ + // public function beginCache($id, $properties = array()) + // { + // $properties['id'] = $id; + // $cache = $this->beginWidget('yii\widgets\OutputCache', $properties); + // if ($cache->getIsContentCached()) { + // $this->endCache(); + // return false; + // } else { + // return true; + // } + // } + // + // /** + // * Ends fragment caching. + // * This is an alias to [[endWidget()]] + // * @see beginCache + // */ + // public function endCache() + // { + // $this->endWidget(); + // } + // + // /** + // * Begins the rendering of content that is to be decorated by the specified view. + // * @param mixed $view the name of the view that will be used to decorate the content. The actual view script + // * is resolved via {@link getViewFile}. If this parameter is null (default), + // * the default layout will be used as the decorative view. + // * Note that if the current controller does not belong to + // * any module, the default layout refers to the application's {@link CWebApplication::layout default layout}; + // * If the controller belongs to a module, the default layout refers to the module's + // * {@link CWebModule::layout default layout}. + // * @param array $params the variables (name=>value) to be extracted and made available in the decorative view. + // * @see endContent + // * @see yii\widgets\ContentDecorator + // */ + // public function beginContent($view, $params = array()) + // { + // $this->beginWidget('yii\widgets\ContentDecorator', array( + // 'view' => $view, + // 'params' => $params, + // )); + // } + // + // /** + // * Ends the rendering of content. + // * @see beginContent + // */ + // public function endContent() + // { + // $this->endWidget(); + // } } \ No newline at end of file diff --git a/framework/base/Widget.php b/framework/base/Widget.php index 2f8264c..a9fe092 100644 --- a/framework/base/Widget.php +++ b/framework/base/Widget.php @@ -7,6 +7,9 @@ namespace yii\base; +use Yii; +use yii\util\FileHelper; + /** * Widget is the base class for widgets. * @@ -70,35 +73,27 @@ class Widget extends Component /** * Renders a view. - * - * The method first finds the actual view file corresponding to the specified view. - * It then calls [[renderFile()]] to render the view file. The rendering result is returned - * as a string. If the view file does not exist, an exception will be thrown. - * - * To determine which view file should be rendered, the method calls [[findViewFile()]] which - * will search in the directories as specified by [[basePath]]. - * - * View name can be a path alias representing an absolute file path (e.g. `@app/views/layout/index`), - * or a path relative to [[basePath]]. The file suffix is optional and defaults to `.php` if not given - * in the view name. - * - * @param string $view the view to be rendered. This can be either a path alias or a path relative to [[basePath]]. - * @param array $params the parameters that should be made available in the view. The PHP function `extract()` - * will be called on this variable to extract the variables from this parameter. - * @return string the rendering result - * @throws Exception if the view file cannot be found + * @param string $view the view name. Please refer to [[findViewFile()]] on how to specify a view name. + * @param array $params the parameters (name-value pairs) that should be made available in the view. + * @return string the rendering result. + * @throws InvalidParamException if the view file does not exist. */ public function render($view, $params = array()) { - return $this->createView()->renderPartial($view, $params); + $file = $this->findViewFile($view); + return Yii::$app->getView()->render($this, $file, $params); } /** - * @return View + * Renders a view file. + * @param string $file the view file to be rendered. This can be either a file path or a path alias. + * @param array $params the parameters (name-value pairs) that should be made available in the view. + * @return string the rendering result. + * @throws InvalidParamException if the view file does not exist. */ - public function createView() + public function renderFile($file, $params = array()) { - return new View($this); + return Yii::$app->getView()->render($this, $file, $params); } /** @@ -112,4 +107,44 @@ class Widget extends Component $class = new \ReflectionClass($className); return dirname($class->getFileName()) . DIRECTORY_SEPARATOR . 'views'; } + + /** + * Finds the view file based on the given view name. + * + * The view name can be specified in one of the following formats: + * + * - path alias (e.g. "@app/views/site/index"); + * - absolute path within application (e.g. "//site/index"): the view name starts with double slashes. + * The actual view file will be looked for under the [[Application::viewPath|view path]] of the application. + * - absolute path within module (e.g. "/site/index"): the view name starts with a single slash. + * The actual view file will be looked for under the [[Module::viewPath|view path]] of the currently + * active module. + * - relative path (e.g. "index"): the actual view file will be looked for under [[viewPath]]. + * + * If the view name does not contain a file extension, it will use the default one `.php`. + * + * @param string $view the view name or the path alias of the view file. + * @return string the view file path. Note that the file may not exist. + * @throws InvalidParamException if the view file is an invalid path alias + */ + public function findViewFile($view) + { + if (strncmp($view, '@', 1) === 0) { + // e.g. "@app/views/common" + $file = Yii::getAlias($view); + } elseif (strncmp($view, '/', 1) !== 0) { + // e.g. "index" + $file = $this->getViewPath() . DIRECTORY_SEPARATOR . $view; + } elseif (strncmp($view, '//', 2) !== 0 && Yii::$app->controller !== null) { + // e.g. "/site/index" + $file = Yii::$app->controller->module->getViewPath() . DIRECTORY_SEPARATOR . ltrim($view, '/'); + } else { + // e.g. "//layouts/main" + $file = Yii::$app->getViewPath() . DIRECTORY_SEPARATOR . ltrim($view, '/'); + } + if (FileHelper::getExtension($file) === '') { + $file .= '.php'; + } + return $file; + } } \ No newline at end of file diff --git a/framework/caching/FileCache.php b/framework/caching/FileCache.php index 2c7bf0a..e565cad 100644 --- a/framework/caching/FileCache.php +++ b/framework/caching/FileCache.php @@ -52,9 +52,6 @@ class FileCache extends Cache { parent::init(); $this->cachePath = \Yii::getAlias($this->cachePath); - if ($this->cachePath === false) { - throw new InvalidConfigException('FileCache.cachePath must be a valid path alias.'); - } if (!is_dir($this->cachePath)) { mkdir($this->cachePath, 0777, true); } diff --git a/framework/console/controllers/MigrateController.php b/framework/console/controllers/MigrateController.php index aec198c..06e100b 100644 --- a/framework/console/controllers/MigrateController.php +++ b/framework/console/controllers/MigrateController.php @@ -114,7 +114,7 @@ class MigrateController extends Controller { if (parent::beforeAction($action)) { $path = Yii::getAlias($this->migrationPath); - if ($path === false || !is_dir($path)) { + if (!is_dir($path)) { throw new Exception("The migration directory \"{$this->migrationPath}\" does not exist."); } $this->migrationPath = $path; diff --git a/framework/util/FileHelper.php b/framework/util/FileHelper.php index e9af8c6..9108476 100644 --- a/framework/util/FileHelper.php +++ b/framework/util/FileHelper.php @@ -43,7 +43,7 @@ class FileHelper public static function ensureDirectory($path) { $p = \Yii::getAlias($path); - if ($p !== false && ($p = realpath($p)) !== false && is_dir($p)) { + if (($p = realpath($p)) !== false && is_dir($p)) { return $p; } else { throw new InvalidConfigException('Directory does not exist: ' . $path); diff --git a/framework/views/error.php b/framework/views/error.php index 28c50f1..893640a 100644 --- a/framework/views/error.php +++ b/framework/views/error.php @@ -1,10 +1,10 @@ owner; -$title = $owner->htmlEncode($exception instanceof \yii\base\Exception || $exception instanceof \yii\base\ErrorException ? $exception->getName() : get_class($exception)); +$context = $this->context; +$title = $context->htmlEncode($exception instanceof \yii\base\Exception || $exception instanceof \yii\base\ErrorException ? $exception->getName() : get_class($exception)); ?> @@ -52,7 +52,7 @@ $title = $owner->htmlEncode($exception instanceof \yii\base\Exception || $except

              -

              htmlEncode($exception->getMessage()))?>

              +

              htmlEncode($exception->getMessage()))?>

              The above error occurred while the Web server was processing your request.

              @@ -61,7 +61,7 @@ $title = $owner->htmlEncode($exception instanceof \yii\base\Exception || $except

              - versionInfo : ''?> + versionInfo : ''?>
              \ No newline at end of file diff --git a/framework/views/exception.php b/framework/views/exception.php index 526a270..6257ffe 100644 --- a/framework/views/exception.php +++ b/framework/views/exception.php @@ -1,10 +1,10 @@ owner; -$title = $owner->htmlEncode($exception instanceof \yii\base\Exception || $exception instanceof \yii\base\ErrorException ? $exception->getName().' ('.get_class($exception).')' : get_class($exception)); +$context = $this->context; +$title = $context->htmlEncode($exception instanceof \yii\base\Exception || $exception instanceof \yii\base\ErrorException ? $exception->getName().' ('.get_class($exception).')' : get_class($exception)); ?> @@ -164,26 +164,26 @@ $title = $owner->htmlEncode($exception instanceof \yii\base\Exception || $except

              - htmlEncode($exception->getMessage()))?> + htmlEncode($exception->getMessage()))?>

              - htmlEncode($exception->getFile()) . '(' . $exception->getLine() . ')'?> + htmlEncode($exception->getFile()) . '(' . $exception->getLine() . ')'?>

              - renderSourceCode($exception->getFile(), $exception->getLine(), $owner->maxSourceLines)?> + renderSourceCode($exception->getFile(), $exception->getLine(), $context->maxSourceLines)?>

              Stack Trace

              - renderTrace($exception->getTrace())?> + renderTrace($exception->getTrace())?>
              - versionInfo : ''?> + versionInfo : ''?>
              diff --git a/framework/web/Session.php b/framework/web/Session.php index 14bea3f..5697679 100644 --- a/framework/web/Session.php +++ b/framework/web/Session.php @@ -214,7 +214,7 @@ class Session extends Component implements \IteratorAggregate, \ArrayAccess, \Co public function setSavePath($value) { $path = Yii::getAlias($value); - if ($path !== false && is_dir($path)) { + if (is_dir($path)) { session_save_path($path); } else { throw new InvalidParamException("Session save path is not a valid directory: $value"); From 2ecf1d92842913246c9b02c3350e6ca6c642004d Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Fri, 22 Mar 2013 08:50:57 -0400 Subject: [PATCH 73/84] view sip --- framework/base/Controller.php | 4 +-- framework/base/View.php | 59 +++++++++++++++++++++++++++++++++---------- 2 files changed, 47 insertions(+), 16 deletions(-) diff --git a/framework/base/Controller.php b/framework/base/Controller.php index 8840cca..3069ce3 100644 --- a/framework/base/Controller.php +++ b/framework/base/Controller.php @@ -414,12 +414,10 @@ class Controller extends Component */ protected function findLayoutFile() { - /** @var $module Module */ + $module = $this->module; if (is_string($this->layout)) { - $module = $this->module; $view = $this->layout; } elseif ($this->layout === null) { - $module = $this->module; while ($module !== null && $module->layout === null) { $module = $module->module; } diff --git a/framework/base/View.php b/framework/base/View.php index 75a911d..9911cd7 100644 --- a/framework/base/View.php +++ b/framework/base/View.php @@ -79,20 +79,10 @@ class View extends Component * @return string the rendering result * @throws InvalidParamException if the view file or the layout file does not exist. */ - public function render($context, $viewFile, $params = array(), $layoutFile = false) + public function render($view, $params = array()) { - $oldContext = $this->context; - $this->context = $context; - - $content = $this->renderFile($viewFile, $params); - - if ($layoutFile !== false) { - $content = $this->renderFile($layoutFile, array('content' => $content)); - } - - $this->context = $oldContext; - - return $content; + $viewFile = $this->findViewFile($this->context, $view); + return $this->renderFile($viewFile, $params); } /** @@ -156,6 +146,49 @@ class View extends Component } /** + * Finds the view file based on the given view name. + * + * A view name can be specified in one of the following formats: + * + * - path alias (e.g. "@app/views/site/index"); + * - absolute path within application (e.g. "//site/index"): the view name starts with double slashes. + * The actual view file will be looked for under the [[Application::viewPath|view path]] of the application. + * - absolute path within module (e.g. "/site/index"): the view name starts with a single slash. + * The actual view file will be looked for under the [[Module::viewPath|view path]] of the currently + * active module. + * - relative path (e.g. "index"): the actual view file will be looked for under [[viewPath]]. + * + * If the view name does not contain a file extension, it will use the default one `.php`. + * + * @param string $view the view name or the path alias of the view file. + * @return string the view file path. Note that the file may not exist. + * @throws InvalidParamException if the view file is an invalid path alias + */ + protected function findViewFile($context, $view) + { + if (FileHelper::getExtension($view) === '') { + $view .= '.php'; + } + + if (strncmp($view, '//', 2) === 0) { + // e.g. "//layouts/main" + $file = Yii::$app->getViewPath() . DIRECTORY_SEPARATOR . ltrim($view, '/'); + } elseif (strncmp($view, '/', 1) === 0) { + // e.g. "/site/index" + $file = Yii::$app->controller->getViewPath() . DIRECTORY_SEPARATOR . ltrim($view, '/'); + } elseif (strncmp($view, '@', 1) !== 0) { + // e.g. "index" or "view/item" + if (method_exists($context, 'getViewPath')) { + $file = $context->getViewPath() . DIRECTORY_SEPARATOR . $view; + } else { + $file = Yii::$app->getViewPath() . DIRECTORY_SEPARATOR . $view; + } + } + + return $file; + } + + /** * Creates a widget. * This method will use [[Yii::createObject()]] to create the widget. * @param string $class the widget class name or path alias From a5ee5b842b29da5afebb6b6e5d2f1cd43161d9bc Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Fri, 22 Mar 2013 17:22:29 -0400 Subject: [PATCH 74/84] view WIP --- framework/base/Controller.php | 52 ++------- framework/base/View.php | 188 ++++++++++++++++++--------------- framework/base/Widget.php | 45 +------- framework/widgets/Clip.php | 57 ++++++++++ framework/widgets/ContentDecorator.php | 81 ++++++++++++++ 5 files changed, 249 insertions(+), 174 deletions(-) create mode 100644 framework/widgets/Clip.php create mode 100644 framework/widgets/ContentDecorator.php diff --git a/framework/base/Controller.php b/framework/base/Controller.php index 3069ce3..9219904 100644 --- a/framework/base/Controller.php +++ b/framework/base/Controller.php @@ -304,9 +304,13 @@ class Controller extends Component */ public function render($view, $params = array()) { - $viewFile = $this->findViewFile($view); + $output = Yii::$app->getView()->render($view, $params, $this); $layoutFile = $this->findLayoutFile(); - return Yii::$app->getView()->render($this, $viewFile, $params, $layoutFile); + if ($layoutFile !== false) { + return Yii::$app->getView()->renderFile($layoutFile, array('content' => $output), $this); + } else { + return $output; + } } /** @@ -319,7 +323,7 @@ class Controller extends Component */ public function renderPartial($view, $params = array()) { - return $this->renderFile($this->findViewFile($view), $params); + return Yii::$app->getView()->render($view, $params, $this); } /** @@ -331,7 +335,7 @@ class Controller extends Component */ public function renderFile($file, $params = array()) { - return Yii::$app->getView()->render($this, $file, $params); + return Yii::$app->getView()->renderFile($file, $params, $this); } /** @@ -346,46 +350,6 @@ class Controller extends Component } /** - * Finds the view file based on the given view name. - * - * A view name can be specified in one of the following formats: - * - * - path alias (e.g. "@app/views/site/index"); - * - absolute path within application (e.g. "//site/index"): the view name starts with double slashes. - * The actual view file will be looked for under the [[Application::viewPath|view path]] of the application. - * - absolute path within module (e.g. "/site/index"): the view name starts with a single slash. - * The actual view file will be looked for under the [[Module::viewPath|view path]] of the currently - * active module. - * - relative path (e.g. "index"): the actual view file will be looked for under [[viewPath]]. - * - * If the view name does not contain a file extension, it will use the default one `.php`. - * - * @param string $view the view name or the path alias of the view file. - * @return string the view file path. Note that the file may not exist. - * @throws InvalidParamException if the view file is an invalid path alias - */ - protected function findViewFile($view) - { - if (strncmp($view, '@', 1) === 0) { - // e.g. "@app/views/common" - $file = Yii::getAlias($view); - } elseif (strncmp($view, '/', 1) !== 0) { - // e.g. "index" - $file = $this->getViewPath() . DIRECTORY_SEPARATOR . $view; - } elseif (strncmp($view, '//', 2) !== 0) { - // e.g. "/site/index" - $file = $this->module->getViewPath() . DIRECTORY_SEPARATOR . ltrim($view, '/'); - } else { - // e.g. "//layouts/main" - $file = Yii::$app->getViewPath() . DIRECTORY_SEPARATOR . ltrim($view, '/'); - } - if (FileHelper::getExtension($file) === '') { - $file .= '.php'; - } - return $file; - } - - /** * Finds the applicable layout file. * * This method locates an applicable layout file via two steps. diff --git a/framework/base/View.php b/framework/base/View.php index 9911cd7..709b761 100644 --- a/framework/base/View.php +++ b/framework/base/View.php @@ -39,6 +39,12 @@ class View extends Component * 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 + * through this property. + */ + public $clips; /** * @var Widget[] the widgets that are currently not ended @@ -61,35 +67,29 @@ class View extends Component } /** - * Renders a view file under a context with an optional layout. + * Renders a view. * - * This method is similar to [[renderFile()]] except that it will update [[context]] - * with the provided $context parameter. It will also apply layout to the rendering result - * of the view file if $layoutFile is given. + * This method will call [[findViewFile()]] to convert the view name into the corresponding view + * file path, and it will then call [[renderFile()]] to render the view. * - * Theming and localization will be performed for the view file and the layout file, if possible. - * - * @param object $context the context object for rendering the file. This could be a controller, a widget, - * or any other object that serves as the rendering context of the view file. In the view file, - * it can be accessed through the [[context]] property. - * @param string $viewFile the view file. This can be a file path or a path alias. + * @param string $view the view name. Please refer to [[findViewFile()]] on how to specify this parameter. * @param array $params the parameters (name-value pairs) that will be extracted and made available in the view file. - * @param string|boolean $layoutFile the layout file. This can be a file path or a path alias. - * If it is false, it means no layout should be applied. + * @param object $context the context that the view should use for rendering the view. If null, + * existing [[context]] will be used. * @return string the rendering result - * @throws InvalidParamException if the view file or the layout file does not exist. + * @throws InvalidParamException if the view cannot be resolved or the view file does not exist. + * @see renderFile + * @see findViewFile */ - public function render($view, $params = array()) + public function render($view, $params = array(), $context = null) { - $viewFile = $this->findViewFile($this->context, $view); - return $this->renderFile($viewFile, $params); + $viewFile = $this->findViewFile($context, $view); + return $this->renderFile($viewFile, $params, $context); } /** * Renders a view file. * - * This method renders the specified view file under the existing [[context]]. - * * If [[theme]] is enabled (not null), it will try to render the themed version of the view file as long * as it is available. * @@ -99,12 +99,14 @@ class View extends Component * Otherwise, it will simply include the view file as a normal PHP file, capture its output and * return it as a string. * - * @param string $viewFile the view file. This can be a file path or a path alias. + * @param string $viewFile the view file. This can be either a file path or a path alias. * @param array $params the parameters (name-value pairs) that will be extracted and made available in the view file. + * @param object $context the context that the view should use for rendering the view. If null, + * existing [[context]] will be used. * @return string the rendering result * @throws InvalidParamException if the view file does not exist */ - public function renderFile($viewFile, $params = array()) + public function renderFile($viewFile, $params = array(), $context = null) { $viewFile = Yii::getAlias($viewFile); if (is_file($viewFile)) { @@ -116,11 +118,18 @@ class View extends Component throw new InvalidParamException("The view file does not exist: $viewFile"); } + $oldContext = $this->context; + $this->context = $context; + if ($this->renderer !== null) { - return $this->renderer->render($this, $viewFile, $params); + $output = $this->renderer->render($this, $viewFile, $params); } else { - return $this->renderPhpFile($viewFile, $params); + $output = $this->renderPhpFile($viewFile, $params); } + + $this->context = $oldContext; + + return $output; } /** @@ -156,36 +165,36 @@ class View extends Component * - absolute path within module (e.g. "/site/index"): the view name starts with a single slash. * The actual view file will be looked for under the [[Module::viewPath|view path]] of the currently * active module. - * - relative path (e.g. "index"): the actual view file will be looked for under [[viewPath]]. + * - relative path (e.g. "index"): the actual view file will be looked for under [[Controller::viewPath|viewPath]] + * of the context object, assuming the context is either a [[Controller]] or a [[Widget]]. * * If the view name does not contain a file extension, it will use the default one `.php`. * + * @param object $context the view context object * @param string $view the view name or the path alias of the view file. * @return string the view file path. Note that the file may not exist. - * @throws InvalidParamException if the view file is an invalid path alias + * @throws InvalidParamException if the view file is an invalid path alias or the context cannot be + * used to determine the actual view file corresponding to the specified view. */ protected function findViewFile($context, $view) { - if (FileHelper::getExtension($view) === '') { - $view .= '.php'; - } - - if (strncmp($view, '//', 2) === 0) { + if (strncmp($view, '@', 1) === 0) { + // e.g. "@app/views/main" + $file = Yii::getAlias($view); + } elseif (strncmp($view, '//', 2) === 0) { // e.g. "//layouts/main" $file = Yii::$app->getViewPath() . DIRECTORY_SEPARATOR . ltrim($view, '/'); } elseif (strncmp($view, '/', 1) === 0) { // e.g. "/site/index" - $file = Yii::$app->controller->getViewPath() . DIRECTORY_SEPARATOR . ltrim($view, '/'); - } elseif (strncmp($view, '@', 1) !== 0) { - // e.g. "index" or "view/item" - if (method_exists($context, 'getViewPath')) { - $file = $context->getViewPath() . DIRECTORY_SEPARATOR . $view; - } else { - $file = Yii::$app->getViewPath() . DIRECTORY_SEPARATOR . $view; - } + $file = Yii::$app->controller->module->getViewPath() . DIRECTORY_SEPARATOR . ltrim($view, '/'); + } elseif ($context instanceof Controller || $context instanceof Widget) { + /** @var $context Controller|Widget */ + $file = $context->getViewPath() . DIRECTORY_SEPARATOR . $view; + } else { + throw new InvalidParamException("Unable to resolve the view file for '$view'."); } - return $file; + return FileHelper::getExtension($file) === '' ? $file . '.php' : $file; } /** @@ -260,26 +269,31 @@ 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 array $properties initial property values for [[yii\widgets\Clip]] - // */ - // public function beginClip($id, $properties = array()) - // { - // $properties['id'] = $id; - // $this->beginWidget('yii\widgets\Clip', $properties); - // } - // - // /** - // * Ends recording a clip. - // */ - // public function endClip() - // { - // $this->endWidget(); - // } + /** + * Begins 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 + */ + public function beginClip($id, $renderInPlace = false) + { + return $this->beginWidget('yii\widgets\Clip', array( + 'id' => $id, + 'renderInPlace' => $renderInPlace, + 'view' => $this, + )); + } + + /** + * Ends recording a clip. + */ + public function endClip() + { + $this->endWidget(); + } + // // /** // * Begins fragment caching. @@ -322,33 +336,33 @@ class View extends Component // $this->endWidget(); // } // - // /** - // * Begins the rendering of content that is to be decorated by the specified view. - // * @param mixed $view the name of the view that will be used to decorate the content. The actual view script - // * is resolved via {@link getViewFile}. If this parameter is null (default), - // * the default layout will be used as the decorative view. - // * Note that if the current controller does not belong to - // * any module, the default layout refers to the application's {@link CWebApplication::layout default layout}; - // * If the controller belongs to a module, the default layout refers to the module's - // * {@link CWebModule::layout default layout}. - // * @param array $params the variables (name=>value) to be extracted and made available in the decorative view. - // * @see endContent - // * @see yii\widgets\ContentDecorator - // */ - // public function beginContent($view, $params = array()) - // { - // $this->beginWidget('yii\widgets\ContentDecorator', array( - // 'view' => $view, - // 'params' => $params, - // )); - // } - // - // /** - // * Ends the rendering of content. - // * @see beginContent - // */ - // public function endContent() - // { - // $this->endWidget(); - // } + /** + * Begins the rendering of content that is to be decorated by the specified view. + * @param mixed $view the name of the view that will be used to decorate the content. The actual view script + * is resolved via {@link getViewFile}. If this parameter is null (default), + * the default layout will be used as the decorative view. + * Note that if the current controller does not belong to + * any module, the default layout refers to the application's {@link CWebApplication::layout default layout}; + * If the controller belongs to a module, the default layout refers to the module's + * {@link CWebModule::layout default layout}. + * @param array $params the variables (name=>value) to be extracted and made available in the decorative view. + * @see endContent + * @see yii\widgets\ContentDecorator + */ + public function beginContent($view, $params = array()) + { + $this->beginWidget('yii\widgets\ContentDecorator', array( + 'view' => $view, + 'params' => $params, + )); + } + + /** + * Ends the rendering of content. + * @see beginContent + */ + public function endContent() + { + $this->endWidget(); + } } \ No newline at end of file diff --git a/framework/base/Widget.php b/framework/base/Widget.php index a9fe092..6b92f09 100644 --- a/framework/base/Widget.php +++ b/framework/base/Widget.php @@ -80,8 +80,7 @@ class Widget extends Component */ public function render($view, $params = array()) { - $file = $this->findViewFile($view); - return Yii::$app->getView()->render($this, $file, $params); + return Yii::$app->getView()->render($view, $params, $this); } /** @@ -93,7 +92,7 @@ class Widget extends Component */ public function renderFile($file, $params = array()) { - return Yii::$app->getView()->render($this, $file, $params); + return Yii::$app->getView()->renderFile($file, $params, $this); } /** @@ -107,44 +106,4 @@ class Widget extends Component $class = new \ReflectionClass($className); return dirname($class->getFileName()) . DIRECTORY_SEPARATOR . 'views'; } - - /** - * Finds the view file based on the given view name. - * - * The view name can be specified in one of the following formats: - * - * - path alias (e.g. "@app/views/site/index"); - * - absolute path within application (e.g. "//site/index"): the view name starts with double slashes. - * The actual view file will be looked for under the [[Application::viewPath|view path]] of the application. - * - absolute path within module (e.g. "/site/index"): the view name starts with a single slash. - * The actual view file will be looked for under the [[Module::viewPath|view path]] of the currently - * active module. - * - relative path (e.g. "index"): the actual view file will be looked for under [[viewPath]]. - * - * If the view name does not contain a file extension, it will use the default one `.php`. - * - * @param string $view the view name or the path alias of the view file. - * @return string the view file path. Note that the file may not exist. - * @throws InvalidParamException if the view file is an invalid path alias - */ - public function findViewFile($view) - { - if (strncmp($view, '@', 1) === 0) { - // e.g. "@app/views/common" - $file = Yii::getAlias($view); - } elseif (strncmp($view, '/', 1) !== 0) { - // e.g. "index" - $file = $this->getViewPath() . DIRECTORY_SEPARATOR . $view; - } elseif (strncmp($view, '//', 2) !== 0 && Yii::$app->controller !== null) { - // e.g. "/site/index" - $file = Yii::$app->controller->module->getViewPath() . DIRECTORY_SEPARATOR . ltrim($view, '/'); - } else { - // e.g. "//layouts/main" - $file = Yii::$app->getViewPath() . DIRECTORY_SEPARATOR . ltrim($view, '/'); - } - if (FileHelper::getExtension($file) === '') { - $file .= '.php'; - } - return $file; - } } \ No newline at end of file diff --git a/framework/widgets/Clip.php b/framework/widgets/Clip.php new file mode 100644 index 0000000..d540b24 --- /dev/null +++ b/framework/widgets/Clip.php @@ -0,0 +1,57 @@ + + * @since 2.0 + */ +class Clip extends Widget +{ + /** + * @var string the ID of this clip. + */ + public $id; + /** + * @var View the view object for keeping the clip. If not set, the view registered with the application + * will be used. + */ + public $view; + /** + * @var boolean whether to render the clip content in place. Defaults to false, + * meaning the captured clip will not be displayed. + */ + public $renderInPlace = false; + + /** + * Starts recording a clip. + */ + public function init() + { + ob_start(); + ob_implicit_flush(false); + } + + /** + * Ends recording a clip. + * This method stops output buffering and saves the rendering result as a named clip in the controller. + */ + public function run() + { + $clip = ob_get_clean(); + if ($this->renderClip) { + echo $clip; + } + $view = $this->view !== null ? $this->view : Yii::$app->getView(); + $view->clips[$this->id] = $clip; + } +} \ No newline at end of file diff --git a/framework/widgets/ContentDecorator.php b/framework/widgets/ContentDecorator.php new file mode 100644 index 0000000..0087698 --- /dev/null +++ b/framework/widgets/ContentDecorator.php @@ -0,0 +1,81 @@ + + * @link http://www.yiiframework.com/ + * @copyright 2008-2013 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +/** + * CContentDecorator decorates the content it encloses with the specified view. + * + * CContentDecorator is mostly used to implement nested layouts, i.e., a layout + * is embedded within another layout. {@link CBaseController} defines a pair of + * convenient methods to use CContentDecorator: + *
              + * $this->beginContent('path/to/view');
              + * // ... content to be decorated
              + * $this->endContent();
              + * 
              + * + * The property {@link view} specifies the name of the view that is used to + * decorate the content. In the view, the content being decorated may be + * accessed with variable $content. + * + * @author Qiang Xue + * @package system.web.widgets + * @since 1.0 + */ +class CContentDecorator extends COutputProcessor +{ + /** + * @var mixed the name of the view that will be used to decorate the captured content. + * If this property is null (default value), the default layout will be used as + * the decorative view. Note that if the current controller does not belong to + * any module, the default layout refers to the application's {@link CWebApplication::layout default layout}; + * If the controller belongs to a module, the default layout refers to the module's + * {@link CWebModule::layout default layout}. + */ + public $view; + /** + * @var array the variables (name=>value) to be extracted and made available in the decorative view. + */ + public $data=array(); + + /** + * Processes the captured output. + * This method decorates the output with the specified {@link view}. + * @param string $output the captured output to be processed + */ + public function processOutput($output) + { + $output=$this->decorate($output); + parent::processOutput($output); + } + + /** + * Decorates the content by rendering a view and embedding the content in it. + * The content being embedded can be accessed in the view using variable $content + * The decorated content will be displayed directly. + * @param string $content the content to be decorated + * @return string the decorated content + */ + protected function decorate($content) + { + $owner=$this->getOwner(); + if($this->view===null) + $viewFile=Yii::app()->getController()->getLayoutFile(null); + else + $viewFile=$owner->getViewFile($this->view); + if($viewFile!==false) + { + $data=$this->data; + $data['content']=$content; + return $owner->renderFile($viewFile,$data,true); + } + else + return $content; + } +} From 5a6c5ccb4ad042ddf48fd4a1880a87832632f86a Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Sat, 23 Mar 2013 15:28:18 -0400 Subject: [PATCH 75/84] Finished ContentDecorator. --- framework/base/View.php | 59 +++++++++++------------ framework/widgets/ContentDecorator.php | 88 +++++++++++++--------------------- 2 files changed, 62 insertions(+), 85 deletions(-) diff --git a/framework/base/View.php b/framework/base/View.php index 709b761..86020ad 100644 --- a/framework/base/View.php +++ b/framework/base/View.php @@ -119,7 +119,9 @@ class View extends Component } $oldContext = $this->context; - $this->context = $context; + if ($context !== null) { + $this->context = $context; + } if ($this->renderer !== null) { $output = $this->renderer->render($this, $viewFile, $params); @@ -276,6 +278,7 @@ class View extends Component * @param boolean $renderInPlace whether to render the clip content in place. * Defaults to false, meaning the captured clip will not be displayed. * @return \yii\widgets\Clip the Clip widget instance + * @see \yii\widgets\Clip */ public function beginClip($id, $renderInPlace = false) { @@ -294,6 +297,31 @@ class View extends Component $this->endWidget(); } + /** + * Begins the rendering of content that is to be decorated by the specified view. + * @param string $view the name of the view that will be used to decorate the content enclosed by this widget. + * Please refer to [[View::findViewFile()]] on how to set this property. + * @param array $params the variables (name=>value) to be extracted and made available in the decorative view. + * @return \yii\widgets\ContentDecorator the ContentDecorator widget instance + * @see \yii\widgets\ContentDecorator + */ + public function beginContent($view, $params = array()) + { + return $this->beginWidget('yii\widgets\ContentDecorator', array( + 'view' => $this, + 'viewName' => $view, + 'params' => $params, + )); + } + + /** + * Ends the rendering of content. + */ + public function endContent() + { + $this->endWidget(); + } + // // /** // * Begins fragment caching. @@ -336,33 +364,4 @@ class View extends Component // $this->endWidget(); // } // - /** - * Begins the rendering of content that is to be decorated by the specified view. - * @param mixed $view the name of the view that will be used to decorate the content. The actual view script - * is resolved via {@link getViewFile}. If this parameter is null (default), - * the default layout will be used as the decorative view. - * Note that if the current controller does not belong to - * any module, the default layout refers to the application's {@link CWebApplication::layout default layout}; - * If the controller belongs to a module, the default layout refers to the module's - * {@link CWebModule::layout default layout}. - * @param array $params the variables (name=>value) to be extracted and made available in the decorative view. - * @see endContent - * @see yii\widgets\ContentDecorator - */ - public function beginContent($view, $params = array()) - { - $this->beginWidget('yii\widgets\ContentDecorator', array( - 'view' => $view, - 'params' => $params, - )); - } - - /** - * Ends the rendering of content. - * @see beginContent - */ - public function endContent() - { - $this->endWidget(); - } } \ No newline at end of file diff --git a/framework/widgets/ContentDecorator.php b/framework/widgets/ContentDecorator.php index 0087698..4c3ae70 100644 --- a/framework/widgets/ContentDecorator.php +++ b/framework/widgets/ContentDecorator.php @@ -1,81 +1,59 @@ * @link http://www.yiiframework.com/ - * @copyright 2008-2013 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ +namespace yii\widgets; + +use Yii; +use yii\base\InvalidConfigException; +use yii\base\Widget; +use yii\base\View; + /** - * CContentDecorator decorates the content it encloses with the specified view. - * - * CContentDecorator is mostly used to implement nested layouts, i.e., a layout - * is embedded within another layout. {@link CBaseController} defines a pair of - * convenient methods to use CContentDecorator: - *
              - * $this->beginContent('path/to/view');
              - * // ... content to be decorated
              - * $this->endContent();
              - * 
              - * - * The property {@link view} specifies the name of the view that is used to - * decorate the content. In the view, the content being decorated may be - * accessed with variable $content. - * * @author Qiang Xue - * @package system.web.widgets - * @since 1.0 + * @since 2.0 */ -class CContentDecorator extends COutputProcessor +class ContentDecorator extends Widget { /** - * @var mixed the name of the view that will be used to decorate the captured content. - * If this property is null (default value), the default layout will be used as - * the decorative view. Note that if the current controller does not belong to - * any module, the default layout refers to the application's {@link CWebApplication::layout default layout}; - * If the controller belongs to a module, the default layout refers to the module's - * {@link CWebModule::layout default layout}. + * @var View the view object for rendering [[viewName]]. If not set, the view registered with the application + * will be used. */ public $view; /** - * @var array the variables (name=>value) to be extracted and made available in the decorative view. + * @var string the name of the view that will be used to decorate the content enclosed by this widget. + * Please refer to [[View::findViewFile()]] on how to set this property. */ - public $data=array(); + public $viewName; + /** + * @var array the parameters (name=>value) to be extracted and made available in the decorative view. + */ + public $params = array(); /** - * Processes the captured output. - * This method decorates the output with the specified {@link view}. - * @param string $output the captured output to be processed + * Starts recording a clip. */ - public function processOutput($output) + public function init() { - $output=$this->decorate($output); - parent::processOutput($output); + if ($this->viewName === null) { + throw new InvalidConfigException('ContentDecorator::viewName must be set.'); + } + ob_start(); + ob_implicit_flush(false); } /** - * Decorates the content by rendering a view and embedding the content in it. - * The content being embedded can be accessed in the view using variable $content - * The decorated content will be displayed directly. - * @param string $content the content to be decorated - * @return string the decorated content + * Ends recording a clip. + * This method stops output buffering and saves the rendering result as a named clip in the controller. */ - protected function decorate($content) + public function run() { - $owner=$this->getOwner(); - if($this->view===null) - $viewFile=Yii::app()->getController()->getLayoutFile(null); - else - $viewFile=$owner->getViewFile($this->view); - if($viewFile!==false) - { - $data=$this->data; - $data['content']=$content; - return $owner->renderFile($viewFile,$data,true); - } - else - return $content; + $params = $this->params; + $params['content'] = ob_get_clean(); + $view = $this->view !== null ? $this->view : Yii::$app->getView(); + echo $view->render($this->viewName, $params); } } From 339ac9b49f7d8f1ee00aa49e7d4babe3b2184ebf Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Sat, 23 Mar 2013 15:30:54 -0400 Subject: [PATCH 76/84] renamed util to helpers. --- framework/helpers/ArrayHelper.php | 340 ++++++++++++ framework/helpers/ConsoleColor.php | 478 +++++++++++++++++ framework/helpers/FileHelper.php | 274 ++++++++++ framework/helpers/Html.php | 976 +++++++++++++++++++++++++++++++++++ framework/helpers/SecurityHelper.php | 272 ++++++++++ framework/helpers/StringHelper.php | 125 +++++ framework/helpers/VarDumper.php | 134 +++++ framework/helpers/mimeTypes.php | 187 +++++++ framework/util/ArrayHelper.php | 340 ------------ framework/util/ConsoleColor.php | 478 ----------------- framework/util/FileHelper.php | 274 ---------- framework/util/Html.php | 976 ----------------------------------- framework/util/SecurityHelper.php | 272 ---------- framework/util/StringHelper.php | 125 ----- framework/util/VarDumper.php | 134 ----- framework/util/mimeTypes.php | 187 ------- 16 files changed, 2786 insertions(+), 2786 deletions(-) create mode 100644 framework/helpers/ArrayHelper.php create mode 100644 framework/helpers/ConsoleColor.php create mode 100644 framework/helpers/FileHelper.php create mode 100644 framework/helpers/Html.php create mode 100644 framework/helpers/SecurityHelper.php create mode 100644 framework/helpers/StringHelper.php create mode 100644 framework/helpers/VarDumper.php create mode 100644 framework/helpers/mimeTypes.php delete mode 100644 framework/util/ArrayHelper.php delete mode 100644 framework/util/ConsoleColor.php delete mode 100644 framework/util/FileHelper.php delete mode 100644 framework/util/Html.php delete mode 100644 framework/util/SecurityHelper.php delete mode 100644 framework/util/StringHelper.php delete mode 100644 framework/util/VarDumper.php delete mode 100644 framework/util/mimeTypes.php diff --git a/framework/helpers/ArrayHelper.php b/framework/helpers/ArrayHelper.php new file mode 100644 index 0000000..447d034 --- /dev/null +++ b/framework/helpers/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\util\ArrayHelper::getValue($_POST, 'username'); + * // working with object + * $username = \yii\util\ArrayHelper::getValue($user, 'username'); + * // working with anonymous function + * $fullName = \yii\util\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 new file mode 100644 index 0000000..74aa154 --- /dev/null +++ b/framework/helpers/ConsoleColor.php @@ -0,0 +1,478 @@ + + * @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 ' $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 new file mode 100644 index 0000000..a7b744b --- /dev/null +++ b/framework/helpers/Html.php @@ -0,0 +1,976 @@ + + * @since 2.0 + */ +class Html +{ + /** + * @var boolean whether to close void (empty) elements. Defaults to true. + * @see voidElements + */ + public static $closeVoidElements = true; + /** + * @var array list of void elements (element name => 1) + * @see closeVoidElements + * @see http://www.w3.org/TR/html-markup/syntax.html#void-element + */ + public static $voidElements = array( + 'area' => 1, + 'base' => 1, + 'br' => 1, + 'col' => 1, + 'command' => 1, + 'embed' => 1, + 'hr' => 1, + 'img' => 1, + 'input' => 1, + 'keygen' => 1, + 'link' => 1, + 'meta' => 1, + 'param' => 1, + 'source' => 1, + 'track' => 1, + 'wbr' => 1, + ); + /** + * @var boolean whether to show the values of boolean attributes in element tags. + * If false, only the attribute names will be generated. + * @see booleanAttributes + */ + public static $showBooleanAttributeValues = true; + /** + * @var array list of boolean attributes. The presence of a boolean attribute on + * an element represents the true value, and the absence of the attribute represents the false value. + * @see showBooleanAttributeValues + * @see http://www.w3.org/TR/html5/infrastructure.html#boolean-attributes + */ + public static $booleanAttributes = array( + 'async' => 1, + 'autofocus' => 1, + 'autoplay' => 1, + 'checked' => 1, + 'controls' => 1, + 'declare' => 1, + 'default' => 1, + 'defer' => 1, + 'disabled' => 1, + 'formnovalidate' => 1, + 'hidden' => 1, + 'ismap' => 1, + 'loop' => 1, + 'multiple' => 1, + 'muted' => 1, + 'nohref' => 1, + 'noresize' => 1, + 'novalidate' => 1, + 'open' => 1, + 'readonly' => 1, + 'required' => 1, + 'reversed' => 1, + 'scoped' => 1, + 'seamless' => 1, + 'selected' => 1, + 'typemustmatch' => 1, + ); + /** + * @var array the preferred order of attributes in a tag. This mainly affects the order of the attributes + * that are rendered by [[renderAttributes()]]. + */ + public static $attributeOrder = array( + 'type', + 'id', + 'class', + 'name', + 'value', + + 'href', + 'src', + 'action', + 'method', + + 'selected', + 'checked', + 'readonly', + 'disabled', + 'multiple', + + 'size', + 'maxlength', + 'width', + 'height', + 'rows', + 'cols', + + 'alt', + 'title', + 'rel', + 'media', + ); + + /** + * Encodes special characters into HTML entities. + * The [[yii\base\Application::charset|application charset]] will be used for encoding. + * @param string $content the content to be encoded + * @return string the encoded content + * @see decode + * @see http://www.php.net/manual/en/function.htmlspecialchars.php + */ + public static function encode($content) + { + return htmlspecialchars($content, ENT_QUOTES, Yii::$app->charset); + } + + /** + * Decodes special HTML entities back to the corresponding characters. + * This is the opposite of [[encode()]]. + * @param string $content the content to be decoded + * @return string the decoded content + * @see encode + * @see http://www.php.net/manual/en/function.htmlspecialchars-decode.php + */ + public static function decode($content) + { + return htmlspecialchars_decode($content, ENT_QUOTES); + } + + /** + * Generates a complete HTML tag. + * @param string $name the tag name + * @param string $content the content to be enclosed between the start and end tags. It will not be HTML-encoded. + * If this is coming from end users, you should consider [[encode()]] it to prevent XSS attacks. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated HTML tag + * @see beginTag + * @see endTag + */ + public static function tag($name, $content = '', $options = array()) + { + $html = '<' . $name . static::renderTagAttributes($options); + if (isset(static::$voidElements[strtolower($name)])) { + return $html . (static::$closeVoidElements ? ' />' : '>'); + } else { + return $html . ">$content"; + } + } + + /** + * Generates a start tag. + * @param string $name the tag name + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated start tag + * @see endTag + * @see tag + */ + public static function beginTag($name, $options = array()) + { + return '<' . $name . static::renderTagAttributes($options) . '>'; + } + + /** + * Generates an end tag. + * @param string $name the tag name + * @return string the generated end tag + * @see beginTag + * @see tag + */ + public static function endTag($name) + { + return ""; + } + + /** + * Encloses the given content within a CDATA tag. + * @param string $content the content to be enclosed within the CDATA tag + * @return string the CDATA tag with the enclosed content. + */ + public static function cdata($content) + { + return ''; + } + + /** + * Generates a style tag. + * @param string $content the style content + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * If the options does not contain "type", a "type" attribute with value "text/css" will be used. + * @return string the generated style tag + */ + public static function style($content, $options = array()) + { + if (!isset($options['type'])) { + $options['type'] = 'text/css'; + } + return static::tag('style', "/**/", $options); + } + + /** + * Generates a script tag. + * @param string $content the script content + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * If the options does not contain "type", a "type" attribute with value "text/javascript" will be rendered. + * @return string the generated script tag + */ + public static function script($content, $options = array()) + { + if (!isset($options['type'])) { + $options['type'] = 'text/javascript'; + } + return static::tag('script', "/**/", $options); + } + + /** + * Generates a link tag that refers to an external CSS file. + * @param array|string $url the URL of the external CSS file. This parameter will be processed by [[url()]]. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated link tag + * @see url + */ + public static function cssFile($url, $options = array()) + { + $options['rel'] = 'stylesheet'; + $options['type'] = 'text/css'; + $options['href'] = static::url($url); + return static::tag('link', '', $options); + } + + /** + * Generates a script tag that refers to an external JavaScript file. + * @param string $url the URL of the external JavaScript file. This parameter will be processed by [[url()]]. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated script tag + * @see url + */ + public static function jsFile($url, $options = array()) + { + $options['type'] = 'text/javascript'; + $options['src'] = static::url($url); + return static::tag('script', '', $options); + } + + /** + * Generates a form start tag. + * @param array|string $action the form action URL. This parameter will be processed by [[url()]]. + * @param string $method the form submission method, either "post" or "get" (case-insensitive) + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated form start tag. + * @see endForm + */ + public static function beginForm($action = '', $method = 'post', $options = array()) + { + $action = static::url($action); + + // query parameters in the action are ignored for GET method + // we use hidden fields to add them back + $hiddens = array(); + if (!strcasecmp($method, 'get') && ($pos = strpos($action, '?')) !== false) { + foreach (explode('&', substr($action, $pos + 1)) as $pair) { + if (($pos1 = strpos($pair, '=')) !== false) { + $hiddens[] = static::hiddenInput(urldecode(substr($pair, 0, $pos1)), urldecode(substr($pair, $pos1 + 1))); + } else { + $hiddens[] = static::hiddenInput(urldecode($pair), ''); + } + } + $action = substr($action, 0, $pos); + } + + $options['action'] = $action; + $options['method'] = $method; + $form = static::beginTag('form', $options); + if ($hiddens !== array()) { + $form .= "\n" . implode("\n", $hiddens); + } + + return $form; + } + + /** + * Generates a form end tag. + * @return string the generated tag + * @see beginForm + */ + public static function endForm() + { + return ''; + } + + /** + * Generates a hyperlink tag. + * @param string $text link body. It will NOT be HTML-encoded. Therefore you can pass in HTML code + * such as an image tag. If this is is coming from end users, you should consider [[encode()]] + * it to prevent XSS attacks. + * @param array|string|null $url the URL for the hyperlink tag. This parameter will be processed by [[url()]] + * and will be used for the "href" attribute of the tag. If this parameter is null, the "href" attribute + * will not be generated. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated hyperlink + * @see url + */ + public static function a($text, $url = null, $options = array()) + { + if ($url !== null) { + $options['href'] = static::url($url); + } + return static::tag('a', $text, $options); + } + + /** + * Generates a mailto hyperlink. + * @param string $text link body. It will NOT be HTML-encoded. Therefore you can pass in HTML code + * such as an image tag. If this is is coming from end users, you should consider [[encode()]] + * it to prevent XSS attacks. + * @param string $email email address. If this is null, the first parameter (link body) will be treated + * as the email address and used. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated mailto link + */ + public static function mailto($text, $email = null, $options = array()) + { + return static::a($text, 'mailto:' . ($email === null ? $text : $email), $options); + } + + /** + * Generates an image tag. + * @param string $src the image URL. This parameter will be processed by [[url()]]. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated image tag + */ + public static function img($src, $options = array()) + { + $options['src'] = static::url($src); + if (!isset($options['alt'])) { + $options['alt'] = ''; + } + return static::tag('img', null, $options); + } + + /** + * Generates a label tag. + * @param string $content label text. It will NOT be HTML-encoded. Therefore you can pass in HTML code + * such as an image tag. If this is is coming from end users, you should consider [[encode()]] + * it to prevent XSS attacks. + * @param string $for the ID of the HTML element that this label is associated with. + * If this is null, the "for" attribute will not be generated. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated label tag + */ + public static function label($content, $for = null, $options = array()) + { + $options['for'] = $for; + return static::tag('label', $content, $options); + } + + /** + * Generates a button tag. + * @param string $name the name attribute. If it is null, the name attribute will not be generated. + * @param string $value the value attribute. If it is null, the value attribute will not be generated. + * @param string $content the content enclosed within the button tag. It will NOT be HTML-encoded. + * Therefore you can pass in HTML code such as an image tag. If this is is coming from end users, + * you should consider [[encode()]] it to prevent XSS attacks. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * If the options does not contain "type", a "type" attribute with value "button" will be rendered. + * @return string the generated button tag + */ + public static function button($name = null, $value = null, $content = 'Button', $options = array()) + { + $options['name'] = $name; + $options['value'] = $value; + if (!isset($options['type'])) { + $options['type'] = 'button'; + } + return static::tag('button', $content, $options); + } + + /** + * Generates a submit button tag. + * @param string $name the name attribute. If it is null, the name attribute will not be generated. + * @param string $value the value attribute. If it is null, the value attribute will not be generated. + * @param string $content the content enclosed within the button tag. It will NOT be HTML-encoded. + * Therefore you can pass in HTML code such as an image tag. If this is is coming from end users, + * you should consider [[encode()]] it to prevent XSS attacks. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated submit button tag + */ + public static function submitButton($name = null, $value = null, $content = 'Submit', $options = array()) + { + $options['type'] = 'submit'; + return static::button($name, $value, $content, $options); + } + + /** + * Generates a reset button tag. + * @param string $name the name attribute. If it is null, the name attribute will not be generated. + * @param string $value the value attribute. If it is null, the value attribute will not be generated. + * @param string $content the content enclosed within the button tag. It will NOT be HTML-encoded. + * Therefore you can pass in HTML code such as an image tag. If this is is coming from end users, + * you should consider [[encode()]] it to prevent XSS attacks. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated reset button tag + */ + public static function resetButton($name = null, $value = null, $content = 'Reset', $options = array()) + { + $options['type'] = 'reset'; + return static::button($name, $value, $content, $options); + } + + /** + * Generates an input type of the given type. + * @param string $type the type attribute. + * @param string $name the name attribute. If it is null, the name attribute will not be generated. + * @param string $value the value attribute. If it is null, the value attribute will not be generated. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated input tag + */ + public static function input($type, $name = null, $value = null, $options = array()) + { + $options['type'] = $type; + $options['name'] = $name; + $options['value'] = $value; + return static::tag('input', null, $options); + } + + /** + * Generates an input button. + * @param string $name the name attribute. + * @param string $value the value attribute. If it is null, the value attribute will not be generated. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated button tag + */ + public static function buttonInput($name, $value = 'Button', $options = array()) + { + return static::input('button', $name, $value, $options); + } + + /** + * Generates a submit input button. + * @param string $name the name attribute. If it is null, the name attribute will not be generated. + * @param string $value the value attribute. If it is null, the value attribute will not be generated. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated button tag + */ + public static function submitInput($name = null, $value = 'Submit', $options = array()) + { + return static::input('submit', $name, $value, $options); + } + + /** + * Generates a reset input button. + * @param string $name the name attribute. If it is null, the name attribute will not be generated. + * @param string $value the value attribute. If it is null, the value attribute will not be generated. + * @param array $options the attributes of the button tag. The values will be HTML-encoded using [[encode()]]. + * Attributes whose value is null will be ignored and not put in the tag returned. + * @return string the generated button tag + */ + public static function resetInput($name = null, $value = 'Reset', $options = array()) + { + return static::input('reset', $name, $value, $options); + } + + /** + * Generates a text input field. + * @param string $name the name attribute. + * @param string $value the value attribute. If it is null, the value attribute will not be generated. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated button tag + */ + public static function textInput($name, $value = null, $options = array()) + { + return static::input('text', $name, $value, $options); + } + + /** + * Generates a hidden input field. + * @param string $name the name attribute. + * @param string $value the value attribute. If it is null, the value attribute will not be generated. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated button tag + */ + public static function hiddenInput($name, $value = null, $options = array()) + { + return static::input('hidden', $name, $value, $options); + } + + /** + * Generates a password input field. + * @param string $name the name attribute. + * @param string $value the value attribute. If it is null, the value attribute will not be generated. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated button tag + */ + public static function passwordInput($name, $value = null, $options = array()) + { + return static::input('password', $name, $value, $options); + } + + /** + * Generates a file input field. + * To use a file input field, you should set the enclosing form's "enctype" attribute to + * be "multipart/form-data". After the form is submitted, the uploaded file information + * can be obtained via $_FILES[$name] (see PHP documentation). + * @param string $name the name attribute. + * @param string $value the value attribute. If it is null, the value attribute will not be generated. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated button tag + */ + public static function fileInput($name, $value = null, $options = array()) + { + return static::input('file', $name, $value, $options); + } + + /** + * Generates a text area input. + * @param string $name the input name + * @param string $value the input value. Note that it will be encoded using [[encode()]]. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated text area tag + */ + public static function textarea($name, $value = '', $options = array()) + { + $options['name'] = $name; + return static::tag('textarea', static::encode($value), $options); + } + + /** + * Generates a radio button input. + * @param string $name the name attribute. + * @param boolean $checked whether the radio button should be checked. + * @param string $value the value attribute. If it is null, the value attribute will not be rendered. + * @param array $options the tag options in terms of name-value pairs. The following options are supported: + * + * - uncheck: string, the value associated with the uncheck state of the radio button. When this attribute + * is present, a hidden input will be generated so that if the radio button is not checked and is submitted, + * the value of this attribute will still be submitted to the server via the hidden input. + * + * The rest of the options will be rendered as the attributes of the resulting tag. The values will + * be HTML-encoded using [[encode()]]. If a value is null, the corresponding attribute will not be rendered. + * + * @return string the generated radio button tag + */ + public static function radio($name, $checked = false, $value = '1', $options = array()) + { + $options['checked'] = $checked; + $options['value'] = $value; + if (isset($options['uncheck'])) { + // add a hidden field so that if the radio button is not selected, it still submits a value + $hidden = static::hiddenInput($name, $options['uncheck']); + unset($options['uncheck']); + } else { + $hidden = ''; + } + return $hidden . static::input('radio', $name, $value, $options); + } + + /** + * Generates a checkbox input. + * @param string $name the name attribute. + * @param boolean $checked whether the checkbox should be checked. + * @param string $value the value attribute. If it is null, the value attribute will not be rendered. + * @param array $options the tag options in terms of name-value pairs. The following options are supported: + * + * - uncheck: string, the value associated with the uncheck state of the checkbox. When this attribute + * is present, a hidden input will be generated so that if the checkbox is not checked and is submitted, + * the value of this attribute will still be submitted to the server via the hidden input. + * + * The rest of the options will be rendered as the attributes of the resulting tag. The values will + * be HTML-encoded using [[encode()]]. If a value is null, the corresponding attribute will not be rendered. + * + * @return string the generated checkbox tag + */ + public static function checkbox($name, $checked = false, $value = '1', $options = array()) + { + $options['checked'] = $checked; + $options['value'] = $value; + if (isset($options['uncheck'])) { + // add a hidden field so that if the checkbox is not selected, it still submits a value + $hidden = static::hiddenInput($name, $options['uncheck']); + unset($options['uncheck']); + } else { + $hidden = ''; + } + return $hidden . static::input('checkbox', $name, $value, $options); + } + + /** + * Generates a drop-down list. + * @param string $name the input name + * @param string $selection the selected value + * @param array $items the option data items. The array keys are option values, and the array values + * are the corresponding option labels. The array can also be nested (i.e. some array values are arrays too). + * For each sub-array, an option group will be generated whose label is the key associated with the sub-array. + * If you have a list of data models, you may convert them into the format described above using + * [[\yii\util\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\util\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\util\ArrayHelper::map()]]. + * + * Note, the values and labels will be automatically HTML-encoded by this method, and the blank spaces in + * the labels will also be HTML-encoded. + * @param array $tagOptions the $options parameter that is passed to the [[dropDownList()]] or [[listBox()]] call. + * This method will take out these elements, if any: "prompt", "options" and "groups". See more details + * in [[dropDownList()]] for the explanation of these elements. + * + * @return string the generated list options + */ + public static function renderSelectOptions($selection, $items, &$tagOptions = array()) + { + $lines = array(); + if (isset($tagOptions['prompt'])) { + $prompt = str_replace(' ', ' ', static::encode($tagOptions['prompt'])); + $lines[] = static::tag('option', $prompt, array('value' => '')); + } + + $options = isset($tagOptions['options']) ? $tagOptions['options'] : array(); + $groups = isset($tagOptions['groups']) ? $tagOptions['groups'] : array(); + unset($tagOptions['prompt'], $tagOptions['options'], $tagOptions['groups']); + + foreach ($items as $key => $value) { + if (is_array($value)) { + $groupAttrs = isset($groups[$key]) ? $groups[$key] : array(); + $groupAttrs['label'] = $key; + $attrs = array('options' => $options, 'groups' => $groups); + $content = static::renderSelectOptions($selection, $value, $attrs); + $lines[] = static::tag('optgroup', "\n" . $content . "\n", $groupAttrs); + } else { + $attrs = isset($options[$key]) ? $options[$key] : array(); + $attrs['value'] = $key; + $attrs['selected'] = $selection !== null && + (!is_array($selection) && !strcmp($key, $selection) + || is_array($selection) && in_array($key, $selection)); + $lines[] = static::tag('option', str_replace(' ', ' ', static::encode($value)), $attrs); + } + } + + return implode("\n", $lines); + } + + /** + * Renders the HTML tag attributes. + * Boolean attributes such as s 'checked', 'disabled', 'readonly', will be handled specially + * according to [[booleanAttributes]] and [[showBooleanAttributeValues]]. + * @param array $attributes attributes to be rendered. The attribute values will be HTML-encoded using [[encode()]]. + * Attributes whose value is null will be ignored and not put in the rendering result. + * @return string the rendering result. If the attributes are not empty, they will be rendered + * into a string with a leading white space (such that it can be directly appended to the tag name + * in a tag. If there is no attribute, an empty string will be returned. + */ + public static function renderTagAttributes($attributes) + { + if (count($attributes) > 1) { + $sorted = array(); + foreach (static::$attributeOrder as $name) { + if (isset($attributes[$name])) { + $sorted[$name] = $attributes[$name]; + } + } + $attributes = array_merge($sorted, $attributes); + } + + $html = ''; + foreach ($attributes as $name => $value) { + if (isset(static::$booleanAttributes[strtolower($name)])) { + if ($value || strcasecmp($name, $value) === 0) { + $html .= static::$showBooleanAttributeValues ? " $name=\"$name\"" : " $name"; + } + } elseif ($value !== null) { + $html .= " $name=\"" . static::encode($value) . '"'; + } + } + return $html; + } + + /** + * Normalizes the input parameter to be a valid URL. + * + * If the input parameter + * + * - is an empty string: the currently requested URL will be returned; + * - is a non-empty string: it will be processed by [[Yii::getAlias()]] which, if the string is an alias, + * will be resolved into a URL; + * - is an array: the first array element is considered a route, while the rest of the name-value + * pairs are considered as the parameters to be used for URL creation using [[\yii\base\Application::createUrl()]]. + * Here are some examples: `array('post/index', 'page' => 2)`, `array('index')`. + * + * @param array|string $url the parameter to be used to generate a valid URL + * @return string the normalized URL + * @throws InvalidParamException if the parameter is invalid. + */ + public static function url($url) + { + if (is_array($url)) { + if (isset($url[0])) { + return Yii::$app->createUrl($url[0], array_splice($url, 1)); + } else { + throw new InvalidParamException('The array specifying a URL must contain at least one element.'); + } + } elseif ($url === '') { + return Yii::$app->getRequest()->getUrl(); + } else { + return Yii::getAlias($url); + } + } +} diff --git a/framework/helpers/SecurityHelper.php b/framework/helpers/SecurityHelper.php new file mode 100644 index 0000000..4186681 --- /dev/null +++ b/framework/helpers/SecurityHelper.php @@ -0,0 +1,272 @@ + + * @author Tom Worster + * @since 2.0 + */ +class SecurityHelper +{ + /** + * Encrypts data. + * @param string $data data to be encrypted. + * @param string $key the encryption secret key + * @return string the encrypted data + * @throws Exception if PHP Mcrypt extension is not loaded or failed to be initialized + * @see decrypt() + */ + public static function encrypt($data, $key) + { + $module = static::openCryptModule(); + $key = StringHelper::substr($key, 0, mcrypt_enc_get_key_size($module)); + srand(); + $iv = mcrypt_create_iv(mcrypt_enc_get_iv_size($module), MCRYPT_RAND); + mcrypt_generic_init($module, $key, $iv); + $encrypted = $iv . mcrypt_generic($module, $data); + mcrypt_generic_deinit($module); + mcrypt_module_close($module); + return $encrypted; + } + + /** + * Decrypts data + * @param string $data data to be decrypted. + * @param string $key the decryption secret key + * @return string the decrypted data + * @throws Exception if PHP Mcrypt extension is not loaded or failed to be initialized + * @see encrypt() + */ + public static function decrypt($data, $key) + { + $module = static::openCryptModule(); + $key = StringHelper::substr($key, 0, mcrypt_enc_get_key_size($module)); + $ivSize = mcrypt_enc_get_iv_size($module); + $iv = StringHelper::substr($data, 0, $ivSize); + mcrypt_generic_init($module, $key, $iv); + $decrypted = mdecrypt_generic($module, StringHelper::substr($data, $ivSize, StringHelper::strlen($data))); + mcrypt_generic_deinit($module); + mcrypt_module_close($module); + return rtrim($decrypted, "\0"); + } + + /** + * Prefixes data with a keyed hash value so that it can later be detected if it is tampered. + * @param string $data the data to be protected + * @param string $key the secret key to be used for generating hash + * @param string $algorithm the hashing algorithm (e.g. "md5", "sha1", "sha256", etc.). Call PHP "hash_algos()" + * function to see the supported hashing algorithms on your system. + * @return string the data prefixed with the keyed hash + * @see validateData() + * @see getSecretKey() + */ + public static function hashData($data, $key, $algorithm = 'sha256') + { + return hash_hmac($algorithm, $data, $key) . $data; + } + + /** + * Validates if the given data is tampered. + * @param string $data the data to be validated. The data must be previously + * generated by [[hashData()]]. + * @param string $key the secret key that was previously used to generate the hash for the data in [[hashData()]]. + * @param string $algorithm the hashing algorithm (e.g. "md5", "sha1", "sha256", etc.). Call PHP "hash_algos()" + * function to see the supported hashing algorithms on your system. This must be the same + * as the value passed to [[hashData()]] when generating the hash for the data. + * @return string the real data with the hash stripped off. False if the data is tampered. + * @see hashData() + */ + public static function validateData($data, $key, $algorithm = 'sha256') + { + $hashSize = StringHelper::strlen(hash_hmac($algorithm, 'test', $key)); + $n = StringHelper::strlen($data); + if ($n >= $hashSize) { + $hash = StringHelper::substr($data, 0, $hashSize); + $data2 = StringHelper::substr($data, $hashSize, $n - $hashSize); + return $hash === hash_hmac($algorithm, $data2, $key) ? $data2 : false; + } else { + return false; + } + } + + /** + * Returns a secret key associated with the specified name. + * If the secret key does not exist, a random key will be generated + * and saved in the file "keys.php" under the application's runtime directory + * so that the same secret key can be returned in future requests. + * @param string $name the name that is associated with the secret key + * @param integer $length the length of the key that should be generated if not exists + * @return string the secret key associated with the specified name + */ + public static function getSecretKey($name, $length = 32) + { + static $keys; + $keyFile = Yii::$app->getRuntimePath() . '/keys.php'; + if ($keys === null) { + $keys = is_file($keyFile) ? require($keyFile) : array(); + } + if (!isset($keys[$name])) { + // generate a 32-char random key + $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + $keys[$name] = substr(str_shuffle(str_repeat($chars, 5)), 0, $length); + file_put_contents($keyFile, " 30) { + throw new InvalidParamException('Hash is invalid.'); + } + + $test = crypt($password, $hash); + $n = strlen($test); + if (strlen($test) < 32 || $n !== strlen($hash)) { + return false; + } + + // Use a for-loop to compare two strings to prevent timing attacks. See: + // http://codereview.stackexchange.com/questions/13512 + $check = 0; + for ($i = 0; $i < $n; ++$i) { + $check |= (ord($test[$i]) ^ ord($hash[$i])); + } + + return $check === 0; + } + + /** + * Generates a salt that can be used to generate a password hash. + * + * The PHP [crypt()](http://php.net/manual/en/function.crypt.php) built-in function + * requires, for the Blowfish hash algorithm, a salt string in a specific format: + * "$2a$", "$2x$" or "$2y$", a two digit cost parameter, "$", and 22 characters + * from the alphabet "./0-9A-Za-z". + * + * @param integer $cost the cost parameter + * @return string the random salt value. + * @throws InvalidParamException if the cost parameter is not between 4 and 30 + */ + protected static function generateSalt($cost = 13) + { + $cost = (int)$cost; + if ($cost < 4 || $cost > 30) { + throw new InvalidParamException('Cost must be between 4 and 31.'); + } + + // Get 20 * 8bits of pseudo-random entropy from mt_rand(). + $rand = ''; + for ($i = 0; $i < 20; ++$i) { + $rand .= chr(mt_rand(0, 255)); + } + + // Add the microtime for a little more entropy. + $rand .= microtime(); + // Mix the bits cryptographically into a 20-byte binary string. + $rand = sha1($rand, true); + // Form the prefix that specifies Blowfish algorithm and cost parameter. + $salt = sprintf("$2y$%02d$", $cost); + // Append the random salt data in the required base64 format. + $salt .= str_replace('+', '.', substr(base64_encode($rand), 0, 22)); + return $salt; + } +} \ No newline at end of file diff --git a/framework/helpers/StringHelper.php b/framework/helpers/StringHelper.php new file mode 100644 index 0000000..3874701 --- /dev/null +++ b/framework/helpers/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\util; + +/** + * VarDumper is intended to replace the buggy PHP function var_dump and print_r. + * It can correctly identify the recursively referenced objects in a complex + * object structure. It also has a recursive depth control to avoid indefinite + * recursive display of some peculiar variables. + * + * VarDumper can be used as follows, + * + * ~~~ + * VarDumper::dump($var); + * ~~~ + * + * @author Qiang Xue + * @since 2.0 + */ +class CVarDumper +{ + private static $_objects; + private static $_output; + private static $_depth; + + /** + * Displays a variable. + * This method achieves the similar functionality as var_dump and print_r + * but is more robust when handling complex objects such as Yii controllers. + * @param mixed $var variable to be dumped + * @param integer $depth maximum depth that the dumper should go into the variable. Defaults to 10. + * @param boolean $highlight whether the result should be syntax-highlighted + */ + public static function dump($var, $depth = 10, $highlight = false) + { + echo self::dumpAsString($var, $depth, $highlight); + } + + /** + * Dumps a variable in terms of a string. + * This method achieves the similar functionality as var_dump and print_r + * but is more robust when handling complex objects such as Yii controllers. + * @param mixed $var variable to be dumped + * @param integer $depth maximum depth that the dumper should go into the variable. Defaults to 10. + * @param boolean $highlight whether the result should be syntax-highlighted + * @return string the string representation of the variable + */ + public static function dumpAsString($var, $depth = 10, $highlight = false) + { + self::$_output = ''; + self::$_objects = array(); + self::$_depth = $depth; + self::dumpInternal($var, 0); + if ($highlight) { + $result = highlight_string("/', '', $result, 1); + } + return self::$_output; + } + + /* + * @param mixed $var variable to be dumped + * @param integer $level depth level + */ + private static function dumpInternal($var, $level) + { + switch (gettype($var)) { + case 'boolean': + self::$_output .= $var ? 'true' : 'false'; + break; + case 'integer': + self::$_output .= "$var"; + break; + case 'double': + self::$_output .= "$var"; + break; + case 'string': + self::$_output .= "'" . addslashes($var) . "'"; + break; + case 'resource': + self::$_output .= '{resource}'; + break; + case 'NULL': + self::$_output .= "null"; + break; + case 'unknown type': + self::$_output .= '{unknown}'; + break; + case 'array': + if (self::$_depth <= $level) { + self::$_output .= 'array(...)'; + } elseif (empty($var)) { + self::$_output .= 'array()'; + } else { + $keys = array_keys($var); + $spaces = str_repeat(' ', $level * 4); + self::$_output .= "array\n" . $spaces . '('; + foreach ($keys as $key) { + self::$_output .= "\n" . $spaces . ' '; + self::dumpInternal($key, 0); + self::$_output .= ' => '; + self::dumpInternal($var[$key], $level + 1); + } + self::$_output .= "\n" . $spaces . ')'; + } + break; + case 'object': + if (($id = array_search($var, self::$_objects, true)) !== false) { + self::$_output .= get_class($var) . '#' . ($id + 1) . '(...)'; + } elseif (self::$_depth <= $level) { + self::$_output .= get_class($var) . '(...)'; + } else { + $id = self::$_objects[] = $var; + $className = get_class($var); + $members = (array)$var; + $spaces = str_repeat(' ', $level * 4); + self::$_output .= "$className#$id\n" . $spaces . '('; + foreach ($members as $key => $value) { + $keyDisplay = strtr(trim($key), array("\0" => ':')); + self::$_output .= "\n" . $spaces . " [$keyDisplay] => "; + self::dumpInternal($value, $level + 1); + } + self::$_output .= "\n" . $spaces . ')'; + } + break; + } + } +} \ No newline at end of file diff --git a/framework/helpers/mimeTypes.php b/framework/helpers/mimeTypes.php new file mode 100644 index 0000000..ffdba4b --- /dev/null +++ b/framework/helpers/mimeTypes.php @@ -0,0 +1,187 @@ + 'application/postscript', + 'aif' => 'audio/x-aiff', + 'aifc' => 'audio/x-aiff', + 'aiff' => 'audio/x-aiff', + 'anx' => 'application/annodex', + 'asc' => 'text/plain', + 'au' => 'audio/basic', + 'avi' => 'video/x-msvideo', + 'axa' => 'audio/annodex', + 'axv' => 'video/annodex', + 'bcpio' => 'application/x-bcpio', + 'bin' => 'application/octet-stream', + 'bmp' => 'image/bmp', + 'c' => 'text/plain', + 'cc' => 'text/plain', + 'ccad' => 'application/clariscad', + 'cdf' => 'application/x-netcdf', + 'class' => 'application/octet-stream', + 'cpio' => 'application/x-cpio', + 'cpt' => 'application/mac-compactpro', + 'csh' => 'application/x-csh', + 'css' => 'text/css', + 'dcr' => 'application/x-director', + 'dir' => 'application/x-director', + 'dms' => 'application/octet-stream', + 'doc' => 'application/msword', + 'drw' => 'application/drafting', + 'dvi' => 'application/x-dvi', + 'dwg' => 'application/acad', + 'dxf' => 'application/dxf', + 'dxr' => 'application/x-director', + 'eps' => 'application/postscript', + 'etx' => 'text/x-setext', + 'exe' => 'application/octet-stream', + 'ez' => 'application/andrew-inset', + 'f' => 'text/plain', + 'f90' => 'text/plain', + 'flac' => 'audio/flac', + 'fli' => 'video/x-fli', + 'flv' => 'video/x-flv', + 'gif' => 'image/gif', + 'gtar' => 'application/x-gtar', + 'gz' => 'application/x-gzip', + 'h' => 'text/plain', + 'hdf' => 'application/x-hdf', + 'hh' => 'text/plain', + 'hqx' => 'application/mac-binhex40', + 'htm' => 'text/html', + 'html' => 'text/html', + 'ice' => 'x-conference/x-cooltalk', + 'ief' => 'image/ief', + 'iges' => 'model/iges', + 'igs' => 'model/iges', + 'ips' => 'application/x-ipscript', + 'ipx' => 'application/x-ipix', + 'jpe' => 'image/jpeg', + 'jpeg' => 'image/jpeg', + 'jpg' => 'image/jpeg', + 'js' => 'application/x-javascript', + 'kar' => 'audio/midi', + 'latex' => 'application/x-latex', + 'lha' => 'application/octet-stream', + 'lsp' => 'application/x-lisp', + 'lzh' => 'application/octet-stream', + 'm' => 'text/plain', + 'man' => 'application/x-troff-man', + 'me' => 'application/x-troff-me', + 'mesh' => 'model/mesh', + 'mid' => 'audio/midi', + 'midi' => 'audio/midi', + 'mif' => 'application/vnd.mif', + 'mime' => 'www/mime', + 'mov' => 'video/quicktime', + 'movie' => 'video/x-sgi-movie', + 'mp2' => 'audio/mpeg', + 'mp3' => 'audio/mpeg', + 'mpe' => 'video/mpeg', + 'mpeg' => 'video/mpeg', + 'mpg' => 'video/mpeg', + 'mpga' => 'audio/mpeg', + 'ms' => 'application/x-troff-ms', + 'msh' => 'model/mesh', + 'nc' => 'application/x-netcdf', + 'oga' => 'audio/ogg', + 'ogg' => 'audio/ogg', + 'ogv' => 'video/ogg', + 'ogx' => 'application/ogg', + 'oda' => 'application/oda', + 'pbm' => 'image/x-portable-bitmap', + 'pdb' => 'chemical/x-pdb', + 'pdf' => 'application/pdf', + 'pgm' => 'image/x-portable-graymap', + 'pgn' => 'application/x-chess-pgn', + 'png' => 'image/png', + 'pnm' => 'image/x-portable-anymap', + 'pot' => 'application/mspowerpoint', + 'ppm' => 'image/x-portable-pixmap', + 'pps' => 'application/mspowerpoint', + 'ppt' => 'application/mspowerpoint', + 'ppz' => 'application/mspowerpoint', + 'pre' => 'application/x-freelance', + 'prt' => 'application/pro_eng', + 'ps' => 'application/postscript', + 'qt' => 'video/quicktime', + 'ra' => 'audio/x-realaudio', + 'ram' => 'audio/x-pn-realaudio', + 'ras' => 'image/cmu-raster', + 'rgb' => 'image/x-rgb', + 'rm' => 'audio/x-pn-realaudio', + 'roff' => 'application/x-troff', + 'rpm' => 'audio/x-pn-realaudio-plugin', + 'rtf' => 'text/rtf', + 'rtx' => 'text/richtext', + 'scm' => 'application/x-lotusscreencam', + 'set' => 'application/set', + 'sgm' => 'text/sgml', + 'sgml' => 'text/sgml', + 'sh' => 'application/x-sh', + 'shar' => 'application/x-shar', + 'silo' => 'model/mesh', + 'sit' => 'application/x-stuffit', + 'skd' => 'application/x-koan', + 'skm' => 'application/x-koan', + 'skp' => 'application/x-koan', + 'skt' => 'application/x-koan', + 'smi' => 'application/smil', + 'smil' => 'application/smil', + 'snd' => 'audio/basic', + 'sol' => 'application/solids', + 'spl' => 'application/x-futuresplash', + 'spx' => 'audio/ogg', + 'src' => 'application/x-wais-source', + 'step' => 'application/STEP', + 'stl' => 'application/SLA', + 'stp' => 'application/STEP', + 'sv4cpio' => 'application/x-sv4cpio', + 'sv4crc' => 'application/x-sv4crc', + 'swf' => 'application/x-shockwave-flash', + 't' => 'application/x-troff', + 'tar' => 'application/x-tar', + 'tcl' => 'application/x-tcl', + 'tex' => 'application/x-tex', + 'texi' => 'application/x-texinfo', + 'texinfo' => 'application/x-texinfo', + 'tif' => 'image/tiff', + 'tiff' => 'image/tiff', + 'tr' => 'application/x-troff', + 'tsi' => 'audio/TSP-audio', + 'tsp' => 'application/dsptype', + 'tsv' => 'text/tab-separated-values', + 'txt' => 'text/plain', + 'unv' => 'application/i-deas', + 'ustar' => 'application/x-ustar', + 'vcd' => 'application/x-cdlink', + 'vda' => 'application/vda', + 'viv' => 'video/vnd.vivo', + 'vivo' => 'video/vnd.vivo', + 'vrml' => 'model/vrml', + 'wav' => 'audio/x-wav', + 'wrl' => 'model/vrml', + 'xbm' => 'image/x-xbitmap', + 'xlc' => 'application/vnd.ms-excel', + 'xll' => 'application/vnd.ms-excel', + 'xlm' => 'application/vnd.ms-excel', + 'xls' => 'application/vnd.ms-excel', + 'xlw' => 'application/vnd.ms-excel', + 'xml' => 'application/xml', + 'xpm' => 'image/x-xpixmap', + 'xspf' => 'application/xspf+xml', + 'xwd' => 'image/x-xwindowdump', + 'xyz' => 'chemical/x-pdb', + 'zip' => 'application/zip', +); diff --git a/framework/util/ArrayHelper.php b/framework/util/ArrayHelper.php deleted file mode 100644 index 447d034..0000000 --- a/framework/util/ArrayHelper.php +++ /dev/null @@ -1,340 +0,0 @@ - - * @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\util\ArrayHelper::getValue($_POST, 'username'); - * // working with object - * $username = \yii\util\ArrayHelper::getValue($user, 'username'); - * // working with anonymous function - * $fullName = \yii\util\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/util/ConsoleColor.php b/framework/util/ConsoleColor.php deleted file mode 100644 index 74aa154..0000000 --- a/framework/util/ConsoleColor.php +++ /dev/null @@ -1,478 +0,0 @@ - - * @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 ' $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/util/Html.php b/framework/util/Html.php deleted file mode 100644 index a7b744b..0000000 --- a/framework/util/Html.php +++ /dev/null @@ -1,976 +0,0 @@ - - * @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\util\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\util\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\util\ArrayHelper::map()]]. - * - * Note, the values and labels will be automatically HTML-encoded by this method, and the blank spaces in - * the labels will also be HTML-encoded. - * @param array $tagOptions the $options parameter that is passed to the [[dropDownList()]] or [[listBox()]] call. - * This method will take out these elements, if any: "prompt", "options" and "groups". See more details - * in [[dropDownList()]] for the explanation of these elements. - * - * @return string the generated list options - */ - public static function renderSelectOptions($selection, $items, &$tagOptions = array()) - { - $lines = array(); - if (isset($tagOptions['prompt'])) { - $prompt = str_replace(' ', ' ', static::encode($tagOptions['prompt'])); - $lines[] = static::tag('option', $prompt, array('value' => '')); - } - - $options = isset($tagOptions['options']) ? $tagOptions['options'] : array(); - $groups = isset($tagOptions['groups']) ? $tagOptions['groups'] : array(); - unset($tagOptions['prompt'], $tagOptions['options'], $tagOptions['groups']); - - foreach ($items as $key => $value) { - if (is_array($value)) { - $groupAttrs = isset($groups[$key]) ? $groups[$key] : array(); - $groupAttrs['label'] = $key; - $attrs = array('options' => $options, 'groups' => $groups); - $content = static::renderSelectOptions($selection, $value, $attrs); - $lines[] = static::tag('optgroup', "\n" . $content . "\n", $groupAttrs); - } else { - $attrs = isset($options[$key]) ? $options[$key] : array(); - $attrs['value'] = $key; - $attrs['selected'] = $selection !== null && - (!is_array($selection) && !strcmp($key, $selection) - || is_array($selection) && in_array($key, $selection)); - $lines[] = static::tag('option', str_replace(' ', ' ', static::encode($value)), $attrs); - } - } - - return implode("\n", $lines); - } - - /** - * Renders the HTML tag attributes. - * Boolean attributes such as s 'checked', 'disabled', 'readonly', will be handled specially - * according to [[booleanAttributes]] and [[showBooleanAttributeValues]]. - * @param array $attributes attributes to be rendered. The attribute values will be HTML-encoded using [[encode()]]. - * Attributes whose value is null will be ignored and not put in the rendering result. - * @return string the rendering result. If the attributes are not empty, they will be rendered - * into a string with a leading white space (such that it can be directly appended to the tag name - * in a tag. If there is no attribute, an empty string will be returned. - */ - public static function renderTagAttributes($attributes) - { - if (count($attributes) > 1) { - $sorted = array(); - foreach (static::$attributeOrder as $name) { - if (isset($attributes[$name])) { - $sorted[$name] = $attributes[$name]; - } - } - $attributes = array_merge($sorted, $attributes); - } - - $html = ''; - foreach ($attributes as $name => $value) { - if (isset(static::$booleanAttributes[strtolower($name)])) { - if ($value || strcasecmp($name, $value) === 0) { - $html .= static::$showBooleanAttributeValues ? " $name=\"$name\"" : " $name"; - } - } elseif ($value !== null) { - $html .= " $name=\"" . static::encode($value) . '"'; - } - } - return $html; - } - - /** - * Normalizes the input parameter to be a valid URL. - * - * If the input parameter - * - * - is an empty string: the currently requested URL will be returned; - * - is a non-empty string: it will be processed by [[Yii::getAlias()]] which, if the string is an alias, - * will be resolved into a URL; - * - is an array: the first array element is considered a route, while the rest of the name-value - * pairs are considered as the parameters to be used for URL creation using [[\yii\base\Application::createUrl()]]. - * Here are some examples: `array('post/index', 'page' => 2)`, `array('index')`. - * - * @param array|string $url the parameter to be used to generate a valid URL - * @return string the normalized URL - * @throws InvalidParamException if the parameter is invalid. - */ - public static function url($url) - { - if (is_array($url)) { - if (isset($url[0])) { - return Yii::$app->createUrl($url[0], array_splice($url, 1)); - } else { - throw new InvalidParamException('The array specifying a URL must contain at least one element.'); - } - } elseif ($url === '') { - return Yii::$app->getRequest()->getUrl(); - } else { - return Yii::getAlias($url); - } - } -} diff --git a/framework/util/SecurityHelper.php b/framework/util/SecurityHelper.php deleted file mode 100644 index 4186681..0000000 --- a/framework/util/SecurityHelper.php +++ /dev/null @@ -1,272 +0,0 @@ - - * @author Tom Worster - * @since 2.0 - */ -class SecurityHelper -{ - /** - * Encrypts data. - * @param string $data data to be encrypted. - * @param string $key the encryption secret key - * @return string the encrypted data - * @throws Exception if PHP Mcrypt extension is not loaded or failed to be initialized - * @see decrypt() - */ - public static function encrypt($data, $key) - { - $module = static::openCryptModule(); - $key = StringHelper::substr($key, 0, mcrypt_enc_get_key_size($module)); - srand(); - $iv = mcrypt_create_iv(mcrypt_enc_get_iv_size($module), MCRYPT_RAND); - mcrypt_generic_init($module, $key, $iv); - $encrypted = $iv . mcrypt_generic($module, $data); - mcrypt_generic_deinit($module); - mcrypt_module_close($module); - return $encrypted; - } - - /** - * Decrypts data - * @param string $data data to be decrypted. - * @param string $key the decryption secret key - * @return string the decrypted data - * @throws Exception if PHP Mcrypt extension is not loaded or failed to be initialized - * @see encrypt() - */ - public static function decrypt($data, $key) - { - $module = static::openCryptModule(); - $key = StringHelper::substr($key, 0, mcrypt_enc_get_key_size($module)); - $ivSize = mcrypt_enc_get_iv_size($module); - $iv = StringHelper::substr($data, 0, $ivSize); - mcrypt_generic_init($module, $key, $iv); - $decrypted = mdecrypt_generic($module, StringHelper::substr($data, $ivSize, StringHelper::strlen($data))); - mcrypt_generic_deinit($module); - mcrypt_module_close($module); - return rtrim($decrypted, "\0"); - } - - /** - * Prefixes data with a keyed hash value so that it can later be detected if it is tampered. - * @param string $data the data to be protected - * @param string $key the secret key to be used for generating hash - * @param string $algorithm the hashing algorithm (e.g. "md5", "sha1", "sha256", etc.). Call PHP "hash_algos()" - * function to see the supported hashing algorithms on your system. - * @return string the data prefixed with the keyed hash - * @see validateData() - * @see getSecretKey() - */ - public static function hashData($data, $key, $algorithm = 'sha256') - { - return hash_hmac($algorithm, $data, $key) . $data; - } - - /** - * Validates if the given data is tampered. - * @param string $data the data to be validated. The data must be previously - * generated by [[hashData()]]. - * @param string $key the secret key that was previously used to generate the hash for the data in [[hashData()]]. - * @param string $algorithm the hashing algorithm (e.g. "md5", "sha1", "sha256", etc.). Call PHP "hash_algos()" - * function to see the supported hashing algorithms on your system. This must be the same - * as the value passed to [[hashData()]] when generating the hash for the data. - * @return string the real data with the hash stripped off. False if the data is tampered. - * @see hashData() - */ - public static function validateData($data, $key, $algorithm = 'sha256') - { - $hashSize = StringHelper::strlen(hash_hmac($algorithm, 'test', $key)); - $n = StringHelper::strlen($data); - if ($n >= $hashSize) { - $hash = StringHelper::substr($data, 0, $hashSize); - $data2 = StringHelper::substr($data, $hashSize, $n - $hashSize); - return $hash === hash_hmac($algorithm, $data2, $key) ? $data2 : false; - } else { - return false; - } - } - - /** - * Returns a secret key associated with the specified name. - * If the secret key does not exist, a random key will be generated - * and saved in the file "keys.php" under the application's runtime directory - * so that the same secret key can be returned in future requests. - * @param string $name the name that is associated with the secret key - * @param integer $length the length of the key that should be generated if not exists - * @return string the secret key associated with the specified name - */ - public static function getSecretKey($name, $length = 32) - { - static $keys; - $keyFile = Yii::$app->getRuntimePath() . '/keys.php'; - if ($keys === null) { - $keys = is_file($keyFile) ? require($keyFile) : array(); - } - if (!isset($keys[$name])) { - // generate a 32-char random key - $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; - $keys[$name] = substr(str_shuffle(str_repeat($chars, 5)), 0, $length); - file_put_contents($keyFile, " 30) { - throw new InvalidParamException('Hash is invalid.'); - } - - $test = crypt($password, $hash); - $n = strlen($test); - if (strlen($test) < 32 || $n !== strlen($hash)) { - return false; - } - - // Use a for-loop to compare two strings to prevent timing attacks. See: - // http://codereview.stackexchange.com/questions/13512 - $check = 0; - for ($i = 0; $i < $n; ++$i) { - $check |= (ord($test[$i]) ^ ord($hash[$i])); - } - - return $check === 0; - } - - /** - * Generates a salt that can be used to generate a password hash. - * - * The PHP [crypt()](http://php.net/manual/en/function.crypt.php) built-in function - * requires, for the Blowfish hash algorithm, a salt string in a specific format: - * "$2a$", "$2x$" or "$2y$", a two digit cost parameter, "$", and 22 characters - * from the alphabet "./0-9A-Za-z". - * - * @param integer $cost the cost parameter - * @return string the random salt value. - * @throws InvalidParamException if the cost parameter is not between 4 and 30 - */ - protected static function generateSalt($cost = 13) - { - $cost = (int)$cost; - if ($cost < 4 || $cost > 30) { - throw new InvalidParamException('Cost must be between 4 and 31.'); - } - - // Get 20 * 8bits of pseudo-random entropy from mt_rand(). - $rand = ''; - for ($i = 0; $i < 20; ++$i) { - $rand .= chr(mt_rand(0, 255)); - } - - // Add the microtime for a little more entropy. - $rand .= microtime(); - // Mix the bits cryptographically into a 20-byte binary string. - $rand = sha1($rand, true); - // Form the prefix that specifies Blowfish algorithm and cost parameter. - $salt = sprintf("$2y$%02d$", $cost); - // Append the random salt data in the required base64 format. - $salt .= str_replace('+', '.', substr(base64_encode($rand), 0, 22)); - return $salt; - } -} \ No newline at end of file diff --git a/framework/util/StringHelper.php b/framework/util/StringHelper.php deleted file mode 100644 index 3874701..0000000 --- a/framework/util/StringHelper.php +++ /dev/null @@ -1,125 +0,0 @@ - - * @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\util; - -/** - * VarDumper is intended to replace the buggy PHP function var_dump and print_r. - * It can correctly identify the recursively referenced objects in a complex - * object structure. It also has a recursive depth control to avoid indefinite - * recursive display of some peculiar variables. - * - * VarDumper can be used as follows, - * - * ~~~ - * VarDumper::dump($var); - * ~~~ - * - * @author Qiang Xue - * @since 2.0 - */ -class CVarDumper -{ - private static $_objects; - private static $_output; - private static $_depth; - - /** - * Displays a variable. - * This method achieves the similar functionality as var_dump and print_r - * but is more robust when handling complex objects such as Yii controllers. - * @param mixed $var variable to be dumped - * @param integer $depth maximum depth that the dumper should go into the variable. Defaults to 10. - * @param boolean $highlight whether the result should be syntax-highlighted - */ - public static function dump($var, $depth = 10, $highlight = false) - { - echo self::dumpAsString($var, $depth, $highlight); - } - - /** - * Dumps a variable in terms of a string. - * This method achieves the similar functionality as var_dump and print_r - * but is more robust when handling complex objects such as Yii controllers. - * @param mixed $var variable to be dumped - * @param integer $depth maximum depth that the dumper should go into the variable. Defaults to 10. - * @param boolean $highlight whether the result should be syntax-highlighted - * @return string the string representation of the variable - */ - public static function dumpAsString($var, $depth = 10, $highlight = false) - { - self::$_output = ''; - self::$_objects = array(); - self::$_depth = $depth; - self::dumpInternal($var, 0); - if ($highlight) { - $result = highlight_string("/', '', $result, 1); - } - return self::$_output; - } - - /* - * @param mixed $var variable to be dumped - * @param integer $level depth level - */ - private static function dumpInternal($var, $level) - { - switch (gettype($var)) { - case 'boolean': - self::$_output .= $var ? 'true' : 'false'; - break; - case 'integer': - self::$_output .= "$var"; - break; - case 'double': - self::$_output .= "$var"; - break; - case 'string': - self::$_output .= "'" . addslashes($var) . "'"; - break; - case 'resource': - self::$_output .= '{resource}'; - break; - case 'NULL': - self::$_output .= "null"; - break; - case 'unknown type': - self::$_output .= '{unknown}'; - break; - case 'array': - if (self::$_depth <= $level) { - self::$_output .= 'array(...)'; - } elseif (empty($var)) { - self::$_output .= 'array()'; - } else { - $keys = array_keys($var); - $spaces = str_repeat(' ', $level * 4); - self::$_output .= "array\n" . $spaces . '('; - foreach ($keys as $key) { - self::$_output .= "\n" . $spaces . ' '; - self::dumpInternal($key, 0); - self::$_output .= ' => '; - self::dumpInternal($var[$key], $level + 1); - } - self::$_output .= "\n" . $spaces . ')'; - } - break; - case 'object': - if (($id = array_search($var, self::$_objects, true)) !== false) { - self::$_output .= get_class($var) . '#' . ($id + 1) . '(...)'; - } elseif (self::$_depth <= $level) { - self::$_output .= get_class($var) . '(...)'; - } else { - $id = self::$_objects[] = $var; - $className = get_class($var); - $members = (array)$var; - $spaces = str_repeat(' ', $level * 4); - self::$_output .= "$className#$id\n" . $spaces . '('; - foreach ($members as $key => $value) { - $keyDisplay = strtr(trim($key), array("\0" => ':')); - self::$_output .= "\n" . $spaces . " [$keyDisplay] => "; - self::dumpInternal($value, $level + 1); - } - self::$_output .= "\n" . $spaces . ')'; - } - break; - } - } -} \ No newline at end of file diff --git a/framework/util/mimeTypes.php b/framework/util/mimeTypes.php deleted file mode 100644 index ffdba4b..0000000 --- a/framework/util/mimeTypes.php +++ /dev/null @@ -1,187 +0,0 @@ - 'application/postscript', - 'aif' => 'audio/x-aiff', - 'aifc' => 'audio/x-aiff', - 'aiff' => 'audio/x-aiff', - 'anx' => 'application/annodex', - 'asc' => 'text/plain', - 'au' => 'audio/basic', - 'avi' => 'video/x-msvideo', - 'axa' => 'audio/annodex', - 'axv' => 'video/annodex', - 'bcpio' => 'application/x-bcpio', - 'bin' => 'application/octet-stream', - 'bmp' => 'image/bmp', - 'c' => 'text/plain', - 'cc' => 'text/plain', - 'ccad' => 'application/clariscad', - 'cdf' => 'application/x-netcdf', - 'class' => 'application/octet-stream', - 'cpio' => 'application/x-cpio', - 'cpt' => 'application/mac-compactpro', - 'csh' => 'application/x-csh', - 'css' => 'text/css', - 'dcr' => 'application/x-director', - 'dir' => 'application/x-director', - 'dms' => 'application/octet-stream', - 'doc' => 'application/msword', - 'drw' => 'application/drafting', - 'dvi' => 'application/x-dvi', - 'dwg' => 'application/acad', - 'dxf' => 'application/dxf', - 'dxr' => 'application/x-director', - 'eps' => 'application/postscript', - 'etx' => 'text/x-setext', - 'exe' => 'application/octet-stream', - 'ez' => 'application/andrew-inset', - 'f' => 'text/plain', - 'f90' => 'text/plain', - 'flac' => 'audio/flac', - 'fli' => 'video/x-fli', - 'flv' => 'video/x-flv', - 'gif' => 'image/gif', - 'gtar' => 'application/x-gtar', - 'gz' => 'application/x-gzip', - 'h' => 'text/plain', - 'hdf' => 'application/x-hdf', - 'hh' => 'text/plain', - 'hqx' => 'application/mac-binhex40', - 'htm' => 'text/html', - 'html' => 'text/html', - 'ice' => 'x-conference/x-cooltalk', - 'ief' => 'image/ief', - 'iges' => 'model/iges', - 'igs' => 'model/iges', - 'ips' => 'application/x-ipscript', - 'ipx' => 'application/x-ipix', - 'jpe' => 'image/jpeg', - 'jpeg' => 'image/jpeg', - 'jpg' => 'image/jpeg', - 'js' => 'application/x-javascript', - 'kar' => 'audio/midi', - 'latex' => 'application/x-latex', - 'lha' => 'application/octet-stream', - 'lsp' => 'application/x-lisp', - 'lzh' => 'application/octet-stream', - 'm' => 'text/plain', - 'man' => 'application/x-troff-man', - 'me' => 'application/x-troff-me', - 'mesh' => 'model/mesh', - 'mid' => 'audio/midi', - 'midi' => 'audio/midi', - 'mif' => 'application/vnd.mif', - 'mime' => 'www/mime', - 'mov' => 'video/quicktime', - 'movie' => 'video/x-sgi-movie', - 'mp2' => 'audio/mpeg', - 'mp3' => 'audio/mpeg', - 'mpe' => 'video/mpeg', - 'mpeg' => 'video/mpeg', - 'mpg' => 'video/mpeg', - 'mpga' => 'audio/mpeg', - 'ms' => 'application/x-troff-ms', - 'msh' => 'model/mesh', - 'nc' => 'application/x-netcdf', - 'oga' => 'audio/ogg', - 'ogg' => 'audio/ogg', - 'ogv' => 'video/ogg', - 'ogx' => 'application/ogg', - 'oda' => 'application/oda', - 'pbm' => 'image/x-portable-bitmap', - 'pdb' => 'chemical/x-pdb', - 'pdf' => 'application/pdf', - 'pgm' => 'image/x-portable-graymap', - 'pgn' => 'application/x-chess-pgn', - 'png' => 'image/png', - 'pnm' => 'image/x-portable-anymap', - 'pot' => 'application/mspowerpoint', - 'ppm' => 'image/x-portable-pixmap', - 'pps' => 'application/mspowerpoint', - 'ppt' => 'application/mspowerpoint', - 'ppz' => 'application/mspowerpoint', - 'pre' => 'application/x-freelance', - 'prt' => 'application/pro_eng', - 'ps' => 'application/postscript', - 'qt' => 'video/quicktime', - 'ra' => 'audio/x-realaudio', - 'ram' => 'audio/x-pn-realaudio', - 'ras' => 'image/cmu-raster', - 'rgb' => 'image/x-rgb', - 'rm' => 'audio/x-pn-realaudio', - 'roff' => 'application/x-troff', - 'rpm' => 'audio/x-pn-realaudio-plugin', - 'rtf' => 'text/rtf', - 'rtx' => 'text/richtext', - 'scm' => 'application/x-lotusscreencam', - 'set' => 'application/set', - 'sgm' => 'text/sgml', - 'sgml' => 'text/sgml', - 'sh' => 'application/x-sh', - 'shar' => 'application/x-shar', - 'silo' => 'model/mesh', - 'sit' => 'application/x-stuffit', - 'skd' => 'application/x-koan', - 'skm' => 'application/x-koan', - 'skp' => 'application/x-koan', - 'skt' => 'application/x-koan', - 'smi' => 'application/smil', - 'smil' => 'application/smil', - 'snd' => 'audio/basic', - 'sol' => 'application/solids', - 'spl' => 'application/x-futuresplash', - 'spx' => 'audio/ogg', - 'src' => 'application/x-wais-source', - 'step' => 'application/STEP', - 'stl' => 'application/SLA', - 'stp' => 'application/STEP', - 'sv4cpio' => 'application/x-sv4cpio', - 'sv4crc' => 'application/x-sv4crc', - 'swf' => 'application/x-shockwave-flash', - 't' => 'application/x-troff', - 'tar' => 'application/x-tar', - 'tcl' => 'application/x-tcl', - 'tex' => 'application/x-tex', - 'texi' => 'application/x-texinfo', - 'texinfo' => 'application/x-texinfo', - 'tif' => 'image/tiff', - 'tiff' => 'image/tiff', - 'tr' => 'application/x-troff', - 'tsi' => 'audio/TSP-audio', - 'tsp' => 'application/dsptype', - 'tsv' => 'text/tab-separated-values', - 'txt' => 'text/plain', - 'unv' => 'application/i-deas', - 'ustar' => 'application/x-ustar', - 'vcd' => 'application/x-cdlink', - 'vda' => 'application/vda', - 'viv' => 'video/vnd.vivo', - 'vivo' => 'video/vnd.vivo', - 'vrml' => 'model/vrml', - 'wav' => 'audio/x-wav', - 'wrl' => 'model/vrml', - 'xbm' => 'image/x-xbitmap', - 'xlc' => 'application/vnd.ms-excel', - 'xll' => 'application/vnd.ms-excel', - 'xlm' => 'application/vnd.ms-excel', - 'xls' => 'application/vnd.ms-excel', - 'xlw' => 'application/vnd.ms-excel', - 'xml' => 'application/xml', - 'xpm' => 'image/x-xpixmap', - 'xspf' => 'application/xspf+xml', - 'xwd' => 'image/x-xwindowdump', - 'xyz' => 'chemical/x-pdb', - 'zip' => 'application/zip', -); From 884977a72452080cfbe582ffe10744350fb4f1ed Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Sat, 23 Mar 2013 15:32:26 -0400 Subject: [PATCH 77/84] renamed util to helpers. --- framework/base/Application.php | 2 +- framework/base/Controller.php | 4 ++-- framework/base/Dictionary.php | 2 +- framework/base/ErrorHandler.php | 2 +- framework/base/Model.php | 2 +- framework/base/Module.php | 4 ++-- framework/base/Theme.php | 2 +- framework/base/View.php | 2 +- framework/base/Widget.php | 2 +- framework/console/controllers/AppController.php | 2 +- framework/console/controllers/HelpController.php | 2 +- framework/console/controllers/MigrateController.php | 2 +- framework/db/ActiveRecord.php | 2 +- framework/helpers/ArrayHelper.php | 8 ++++---- framework/helpers/ConsoleColor.php | 2 +- framework/helpers/FileHelper.php | 2 +- framework/helpers/Html.php | 8 ++++---- framework/helpers/SecurityHelper.php | 2 +- framework/helpers/StringHelper.php | 2 +- framework/helpers/VarDumper.php | 2 +- framework/web/CookieCollection.php | 2 +- framework/web/Response.php | 2 +- framework/web/Sort.php | 2 +- framework/widgets/ActiveForm.php | 6 +++--- tests/unit/framework/util/ArrayHelperTest.php | 2 +- tests/unit/framework/util/HtmlTest.php | 2 +- tests/web/app/protected/controllers/SiteController.php | 2 +- 27 files changed, 37 insertions(+), 37 deletions(-) diff --git a/framework/base/Application.php b/framework/base/Application.php index bbc4601..3dcbb26 100644 --- a/framework/base/Application.php +++ b/framework/base/Application.php @@ -8,7 +8,7 @@ namespace yii\base; use Yii; -use yii\util\FileHelper; +use yii\helpers\FileHelper; /** * Application is the base class for all application classes. diff --git a/framework/base/Controller.php b/framework/base/Controller.php index 9219904..17fb4da 100644 --- a/framework/base/Controller.php +++ b/framework/base/Controller.php @@ -8,8 +8,8 @@ namespace yii\base; use Yii; -use yii\util\FileHelper; -use yii\util\StringHelper; +use yii\helpers\FileHelper; +use yii\helpers\StringHelper; /** * Controller is the base class for classes containing controller logic. diff --git a/framework/base/Dictionary.php b/framework/base/Dictionary.php index 343dbfd..9343d68 100644 --- a/framework/base/Dictionary.php +++ b/framework/base/Dictionary.php @@ -7,7 +7,7 @@ namespace yii\base; -use yii\util\ArrayHelper; +use yii\helpers\ArrayHelper; /** * Dictionary implements a collection that stores key-value pairs. diff --git a/framework/base/ErrorHandler.php b/framework/base/ErrorHandler.php index 996fa18..a3ab137 100644 --- a/framework/base/ErrorHandler.php +++ b/framework/base/ErrorHandler.php @@ -16,7 +16,7 @@ namespace yii\base; * @author Qiang Xue * @since 2.0 */ -use yii\util\VarDumper; +use yii\helpers\VarDumper; class ErrorHandler extends Component { diff --git a/framework/base/Model.php b/framework/base/Model.php index b761ada..402a558 100644 --- a/framework/base/Model.php +++ b/framework/base/Model.php @@ -7,7 +7,7 @@ namespace yii\base; -use yii\util\StringHelper; +use yii\helpers\StringHelper; use yii\validators\Validator; use yii\validators\RequiredValidator; diff --git a/framework/base/Module.php b/framework/base/Module.php index 0e5c1cb..9988164 100644 --- a/framework/base/Module.php +++ b/framework/base/Module.php @@ -8,8 +8,8 @@ namespace yii\base; use Yii; -use yii\util\StringHelper; -use yii\util\FileHelper; +use yii\helpers\StringHelper; +use yii\helpers\FileHelper; /** * Module is the base class for module and application classes. diff --git a/framework/base/Theme.php b/framework/base/Theme.php index c5fd925..88ecb0a 100644 --- a/framework/base/Theme.php +++ b/framework/base/Theme.php @@ -9,7 +9,7 @@ namespace yii\base; use Yii; use yii\base\InvalidConfigException; -use yii\util\FileHelper; +use yii\helpers\FileHelper; /** * Theme represents an application theme. diff --git a/framework/base/View.php b/framework/base/View.php index 86020ad..36c90ad 100644 --- a/framework/base/View.php +++ b/framework/base/View.php @@ -9,7 +9,7 @@ namespace yii\base; use Yii; use yii\base\Application; -use yii\util\FileHelper; +use yii\helpers\FileHelper; /** * View represents a view object in the MVC pattern. diff --git a/framework/base/Widget.php b/framework/base/Widget.php index 6b92f09..24d0685 100644 --- a/framework/base/Widget.php +++ b/framework/base/Widget.php @@ -8,7 +8,7 @@ namespace yii\base; use Yii; -use yii\util\FileHelper; +use yii\helpers\FileHelper; /** * Widget is the base class for widgets. diff --git a/framework/console/controllers/AppController.php b/framework/console/controllers/AppController.php index 78cf6fc..93ef5f5 100644 --- a/framework/console/controllers/AppController.php +++ b/framework/console/controllers/AppController.php @@ -8,7 +8,7 @@ namespace yii\console\controllers; use yii\console\Controller; -use yii\util\FileHelper; +use yii\helpers\FileHelper; use yii\base\Exception; /** diff --git a/framework/console/controllers/HelpController.php b/framework/console/controllers/HelpController.php index 01bc994..ea7e3d5 100644 --- a/framework/console/controllers/HelpController.php +++ b/framework/console/controllers/HelpController.php @@ -13,7 +13,7 @@ use yii\console\Exception; use yii\base\InlineAction; use yii\console\Controller; use yii\console\Request; -use yii\util\StringHelper; +use yii\helpers\StringHelper; /** * This command provides help information about console commands. diff --git a/framework/console/controllers/MigrateController.php b/framework/console/controllers/MigrateController.php index 06e100b..7f9a18f 100644 --- a/framework/console/controllers/MigrateController.php +++ b/framework/console/controllers/MigrateController.php @@ -13,7 +13,7 @@ use yii\console\Exception; use yii\console\Controller; use yii\db\Connection; use yii\db\Query; -use yii\util\ArrayHelper; +use yii\helpers\ArrayHelper; /** * This command manages application migrations. diff --git a/framework/db/ActiveRecord.php b/framework/db/ActiveRecord.php index f8d2b99..0c15121 100644 --- a/framework/db/ActiveRecord.php +++ b/framework/db/ActiveRecord.php @@ -16,7 +16,7 @@ use yii\base\InvalidCallException; use yii\db\Connection; use yii\db\TableSchema; use yii\db\Expression; -use yii\util\StringHelper; +use yii\helpers\StringHelper; /** * ActiveRecord is the base class for classes representing relational data in terms of objects. diff --git a/framework/helpers/ArrayHelper.php b/framework/helpers/ArrayHelper.php index 447d034..65fa962 100644 --- a/framework/helpers/ArrayHelper.php +++ b/framework/helpers/ArrayHelper.php @@ -5,7 +5,7 @@ * @license http://www.yiiframework.com/license/ */ -namespace yii\util; +namespace yii\helpers; use Yii; use yii\base\InvalidParamException; @@ -59,11 +59,11 @@ class ArrayHelper * * ~~~ * // working with array - * $username = \yii\util\ArrayHelper::getValue($_POST, 'username'); + * $username = \yii\helpers\ArrayHelper::getValue($_POST, 'username'); * // working with object - * $username = \yii\util\ArrayHelper::getValue($user, 'username'); + * $username = \yii\helpers\ArrayHelper::getValue($user, 'username'); * // working with anonymous function - * $fullName = \yii\util\ArrayHelper::getValue($user, function($user, $defaultValue) { + * $fullName = \yii\helpers\ArrayHelper::getValue($user, function($user, $defaultValue) { * return $user->firstName . ' ' . $user->lastName; * }); * ~~~ diff --git a/framework/helpers/ConsoleColor.php b/framework/helpers/ConsoleColor.php index 74aa154..c64db24 100644 --- a/framework/helpers/ConsoleColor.php +++ b/framework/helpers/ConsoleColor.php @@ -5,7 +5,7 @@ * @license http://www.yiiframework.com/license/ */ -namespace yii\util; +namespace yii\helpers; // todo define how subclassing will work // todo add a run() or render() method diff --git a/framework/helpers/FileHelper.php b/framework/helpers/FileHelper.php index 9108476..f850b98 100644 --- a/framework/helpers/FileHelper.php +++ b/framework/helpers/FileHelper.php @@ -7,7 +7,7 @@ * @license http://www.yiiframework.com/license/ */ -namespace yii\util; +namespace yii\helpers; use yii\base\Exception; use yii\base\InvalidConfigException; diff --git a/framework/helpers/Html.php b/framework/helpers/Html.php index a7b744b..b004885 100644 --- a/framework/helpers/Html.php +++ b/framework/helpers/Html.php @@ -5,7 +5,7 @@ * @license http://www.yiiframework.com/license/ */ -namespace yii\util; +namespace yii\helpers; use Yii; use yii\base\InvalidParamException; @@ -655,7 +655,7 @@ class Html * 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\util\ArrayHelper::map()]]. + * [[\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. @@ -695,7 +695,7 @@ class Html * 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\util\ArrayHelper::map()]]. + * [[\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. @@ -866,7 +866,7 @@ class Html * 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\util\ArrayHelper::map()]]. + * [[\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. diff --git a/framework/helpers/SecurityHelper.php b/framework/helpers/SecurityHelper.php index 4186681..5029dd6 100644 --- a/framework/helpers/SecurityHelper.php +++ b/framework/helpers/SecurityHelper.php @@ -5,7 +5,7 @@ * @license http://www.yiiframework.com/license/ */ -namespace yii\util; +namespace yii\helpers; use Yii; use yii\base\Exception; diff --git a/framework/helpers/StringHelper.php b/framework/helpers/StringHelper.php index 3874701..ace34db 100644 --- a/framework/helpers/StringHelper.php +++ b/framework/helpers/StringHelper.php @@ -5,7 +5,7 @@ * @license http://www.yiiframework.com/license/ */ -namespace yii\util; +namespace yii\helpers; /** * StringHelper diff --git a/framework/helpers/VarDumper.php b/framework/helpers/VarDumper.php index ae474e6..64c3639 100644 --- a/framework/helpers/VarDumper.php +++ b/framework/helpers/VarDumper.php @@ -6,7 +6,7 @@ * @license http://www.yiiframework.com/license/ */ -namespace yii\util; +namespace yii\helpers; /** * VarDumper is intended to replace the buggy PHP function var_dump and print_r. diff --git a/framework/web/CookieCollection.php b/framework/web/CookieCollection.php index 2577d35..c76926b 100644 --- a/framework/web/CookieCollection.php +++ b/framework/web/CookieCollection.php @@ -9,7 +9,7 @@ namespace yii\web; use Yii; use yii\base\DictionaryIterator; -use yii\util\SecurityHelper; +use yii\helpers\SecurityHelper; /** * CookieCollection maintains the cookies available in the current request. diff --git a/framework/web/Response.php b/framework/web/Response.php index 3a0ce8b..d6659cf 100644 --- a/framework/web/Response.php +++ b/framework/web/Response.php @@ -7,7 +7,7 @@ namespace yii\web; -use yii\util\FileHelper; +use yii\helpers\FileHelper; /** * @author Qiang Xue diff --git a/framework/web/Sort.php b/framework/web/Sort.php index 85e9ce8..7cfeeca 100644 --- a/framework/web/Sort.php +++ b/framework/web/Sort.php @@ -8,7 +8,7 @@ namespace yii\web; use Yii; -use yii\util\Html; +use yii\helpers\Html; /** * Sort represents information relevant to sorting. diff --git a/framework/widgets/ActiveForm.php b/framework/widgets/ActiveForm.php index 2d47253..2c965e7 100644 --- a/framework/widgets/ActiveForm.php +++ b/framework/widgets/ActiveForm.php @@ -11,8 +11,8 @@ use Yii; use yii\base\InvalidParamException; use yii\base\Widget; use yii\base\Model; -use yii\util\Html; -use yii\util\ArrayHelper; +use yii\helpers\Html; +use yii\helpers\ArrayHelper; /** * ActiveForm ... @@ -23,7 +23,7 @@ use yii\util\ArrayHelper; class ActiveForm extends Widget { /** - * @param array|string $action the form action URL. This parameter will be processed by [[\yii\util\Html::url()]]. + * @param array|string $action the form action URL. This parameter will be processed by [[\yii\helpers\Html::url()]]. */ public $action = ''; /** diff --git a/tests/unit/framework/util/ArrayHelperTest.php b/tests/unit/framework/util/ArrayHelperTest.php index a36ce68..117c702 100644 --- a/tests/unit/framework/util/ArrayHelperTest.php +++ b/tests/unit/framework/util/ArrayHelperTest.php @@ -2,7 +2,7 @@ namespace yiiunit\framework\util; -use yii\util\ArrayHelper; +use yii\helpers\ArrayHelper; class ArrayHelperTest extends \yii\test\TestCase { diff --git a/tests/unit/framework/util/HtmlTest.php b/tests/unit/framework/util/HtmlTest.php index a887f29..eba1a20 100644 --- a/tests/unit/framework/util/HtmlTest.php +++ b/tests/unit/framework/util/HtmlTest.php @@ -3,7 +3,7 @@ namespace yiiunit\framework\util; use Yii; -use yii\util\Html; +use yii\helpers\Html; use yii\web\Application; class HtmlTest extends \yii\test\TestCase diff --git a/tests/web/app/protected/controllers/SiteController.php b/tests/web/app/protected/controllers/SiteController.php index 38efce3..050bf90 100644 --- a/tests/web/app/protected/controllers/SiteController.php +++ b/tests/web/app/protected/controllers/SiteController.php @@ -1,6 +1,6 @@ Date: Sun, 24 Mar 2013 22:34:45 -0400 Subject: [PATCH 78/84] Finished new message translation implementation. --- framework/base/ActionFilter.php | 45 ++++++ framework/base/Application.php | 1 - framework/base/View.php | 80 +++++----- framework/i18n/I18N.php | 114 +++++++------- framework/i18n/PhpMessageSource.php | 23 ++- framework/logging/Target.php | 3 +- framework/widgets/FragmentCache.php | 294 ++++++++++++++++++++++++++++++++++++ todo.md | 1 - 8 files changed, 457 insertions(+), 104 deletions(-) create mode 100644 framework/base/ActionFilter.php create mode 100644 framework/widgets/FragmentCache.php diff --git a/framework/base/ActionFilter.php b/framework/base/ActionFilter.php new file mode 100644 index 0000000..2655c5a --- /dev/null +++ b/framework/base/ActionFilter.php @@ -0,0 +1,45 @@ + + * @since 2.0 + */ +class Filter extends Behavior +{ + /** + * Declares event handlers for the [[owner]]'s events. + * @return array events (array keys) and the corresponding event handler methods (array values). + */ + public function events() + { + return array( + 'beforeAction' => 'beforeAction', + 'afterAction' => 'afterAction', + ); + } + + /** + * @param ActionEvent $event + * @return boolean + */ + public function beforeAction($event) + { + return $event->isValid; + } + + /** + * @param ActionEvent $event + * @return boolean + */ + public function afterAction($event) + { + + } +} \ No newline at end of file diff --git a/framework/base/Application.php b/framework/base/Application.php index 3dcbb26..31087e2 100644 --- a/framework/base/Application.php +++ b/framework/base/Application.php @@ -92,7 +92,6 @@ class Application extends Module private $_runtimePath; private $_ended = false; - private $_language; /** * @var string Used to reserve memory for fatal error handler. This memory diff --git a/framework/base/View.php b/framework/base/View.php index 36c90ad..baa1d10 100644 --- a/framework/base/View.php +++ b/framework/base/View.php @@ -322,46 +322,42 @@ class View extends Component $this->endWidget(); } - // - // /** - // * Begins fragment caching. - // * This method will display cached content if it is available. - // * If not, it will start caching and would expect an [[endCache()]] - // * call to end the cache and save the content into cache. - // * A typical usage of fragment caching is as follows, - // * - // * ~~~ - // * if($this->beginCache($id)) { - // * // ...generate content here - // * $this->endCache(); - // * } - // * ~~~ - // * - // * @param string $id a unique ID identifying the fragment to be cached. - // * @param array $properties initial property values for [[yii\widgets\OutputCache]] - // * @return boolean whether we need to generate content for caching. False if cached version is available. - // * @see endCache - // */ - // public function beginCache($id, $properties = array()) - // { - // $properties['id'] = $id; - // $cache = $this->beginWidget('yii\widgets\OutputCache', $properties); - // if ($cache->getIsContentCached()) { - // $this->endCache(); - // return false; - // } else { - // return true; - // } - // } - // - // /** - // * Ends fragment caching. - // * This is an alias to [[endWidget()]] - // * @see beginCache - // */ - // public function endCache() - // { - // $this->endWidget(); - // } - // + /** + * Begins fragment caching. + * This method will display cached content if it is available. + * If not, it will start caching and would expect an [[endCache()]] + * call to end the cache and save the content into cache. + * A typical usage of fragment caching is as follows, + * + * ~~~ + * if($this->beginCache($id)) { + * // ...generate content here + * $this->endCache(); + * } + * ~~~ + * + * @param string $id a unique ID identifying the fragment to be cached. + * @param array $properties initial property values for [[\yii\widgets\OutputCache]] + * @return boolean whether you should generate the content for caching. + * False if the cached version is available. + */ + public function beginCache($id, $properties = array()) + { + $properties['id'] = $id; + $cache = $this->beginWidget('yii\widgets\OutputCache', $properties); + if ($cache->getIsContentCached()) { + $this->endCache(); + return false; + } else { + return true; + } + } + + /** + * Ends fragment caching. + */ + public function endCache() + { + $this->endWidget(); + } } \ No newline at end of file diff --git a/framework/i18n/I18N.php b/framework/i18n/I18N.php index ab87dfc..0409da3 100644 --- a/framework/i18n/I18N.php +++ b/framework/i18n/I18N.php @@ -4,91 +4,93 @@ namespace yii\i18n; use Yii; use yii\base\Component; +use yii\base\InvalidConfigException; class I18N extends Component { + /** + * @var array list of [[MessageSource]] configurations or objects. The array keys are message + * categories, and the array values are the corresponding [[MessageSource]] objects or the configurations + * for creating the [[MessageSource]] objects. The message categories can contain the wildcard '*' at the end + * to match multiple categories with the same prefix. For example, 'app\*' matches both 'app\cat1' and 'app\cat2'. + */ + public $translations; + + public function init() + { + if (!isset($this->translations['yii'])) { + $this->translations['yii'] = array( + 'class' => 'yii\i18n\PhpMessageSource', + 'sourceLanguage' => 'en_US', + 'basePath' => '@yii/messages', + ); + } + if (!isset($this->translations['app'])) { + $this->translations['app'] = array( + 'class' => 'yii\i18n\PhpMessageSource', + 'sourceLanguage' => 'en_US', + 'basePath' => '@app/messages', + ); + } + } + public function translate($message, $params = array(), $language = null) { if ($language === null) { $language = Yii::$app->language; } - if (strpos($message, '|') !== false && preg_match('/^([\w\-\.]+)\|(.*)/', $message, $matches)) { + // allow chars for category: word chars, ".", "-", "/","\" + if (strpos($message, '|') !== false && preg_match('/^([\w\-\\/\.\\\\]+)\|(.*)/', $message, $matches)) { $category = $matches[1]; $message = $matches[2]; } else { $category = 'app'; } -// $message = $this->getMessageSource($category)->translate($category, $message, $language); -// -// if (!is_array($params)) { -// $params = array($params); -// } -// -// if (isset($params[0])) { -// $message = $this->getPluralFormat($message, $params[0], $language); -// if (!isset($params['{n}'])) { -// $params['{n}'] = $params[0]; -// } -// unset($params[0]); -// } + $message = $this->getMessageSource($category)->translate($category, $message, $language); - return $params === array() ? $message : strtr($message, $params); - } + if (!is_array($params)) { + $params = array($params); + } - public function getLocale($language) - { + if (isset($params[0])) { + $message = $this->getPluralForm($message, $params[0], $language); + if (!isset($params['{n}'])) { + $params['{n}'] = $params[0]; + } + unset($params[0]); + } + return $params === array() ? $message : strtr($message, $params); } public function getMessageSource($category) { - return $category === 'yii' ? $this->getMessages() : $this->getCoreMessages(); - } - - private $_coreMessages; - private $_messages; - - public function getCoreMessages() - { - if (is_object($this->_coreMessages)) { - return $this->_coreMessages; - } elseif ($this->_coreMessages === null) { - return $this->_coreMessages = new PhpMessageSource(array( - 'sourceLanguage' => 'en_US', - 'basePath' => '@yii/messages', - )); + if (isset($this->translations[$category])) { + $source = $this->translations[$category]; } else { - return $this->_coreMessages = Yii::createObject($this->_coreMessages); + // try wildcard matching + foreach ($this->translations as $pattern => $config) { + if (substr($pattern, -1) === '*' && strpos($category, rtrim($pattern, '*')) === 0) { + $source = $config; + break; + } + } } - } - - public function setCoreMessages($config) - { - $this->_coreMessages = $config; - } - - public function getMessages() - { - if (is_object($this->_messages)) { - return $this->_messages; - } elseif ($this->_messages === null) { - return $this->_messages = new PhpMessageSource(array( - 'sourceLanguage' => 'en_US', - 'basePath' => '@app/messages', - )); + if (isset($source)) { + return $source instanceof MessageSource ? $source : Yii::createObject($source); } else { - return $this->_messages = Yii::createObject($this->_messages); + throw new InvalidConfigException("Unable to locate message source for category '$category'."); } } - public function setMessages($config) + public function getLocale($language) { - $this->_messages = $config; + } - protected function getPluralFormat($message, $number, $language) + protected function getPluralForm($message, $number, $language) { if (strpos($message, '|') === false) { return $message; @@ -96,7 +98,7 @@ class I18N extends Component $chunks = explode('|', $message); $rules = $this->getLocale($language)->getPluralRules(); foreach ($rules as $i => $rule) { - if (isset($chunks[$i]) && self::evaluate($rule, $number)) { + if (isset($chunks[$i]) && $this->evaluate($rule, $number)) { return $chunks[$i]; } } @@ -110,7 +112,7 @@ class I18N extends Component * @param mixed $n the number value * @return boolean the expression result */ - protected static function evaluate($expression, $n) + protected function evaluate($expression, $n) { return @eval("return $expression;"); } diff --git a/framework/i18n/PhpMessageSource.php b/framework/i18n/PhpMessageSource.php index 5c7374a..6b12353 100644 --- a/framework/i18n/PhpMessageSource.php +++ b/framework/i18n/PhpMessageSource.php @@ -36,6 +36,18 @@ class PhpMessageSource extends MessageSource * the "messages" subdirectory of the application directory (e.g. "protected/messages"). */ public $basePath = '@app/messages'; + /** + * @var array mapping between message categories and the corresponding message file paths. + * The file paths are relative to [[basePath]]. For example, + * + * ~~~ + * array( + * 'core' => 'core.php', + * 'ext' => 'extensions.php', + * ) + * ~~~ + */ + public $fileMap; /** * Loads the message translation for the specified language and category. @@ -45,7 +57,14 @@ class PhpMessageSource extends MessageSource */ protected function loadMessages($category, $language) { - $messageFile = Yii::getAlias($this->basePath) . "/$language/$category.php"; + $messageFile = Yii::getAlias($this->basePath) . "/$language/"; + if (isset($this->fileMap[$category])) { + $messageFile .= $this->fileMap[$category]; + } elseif (($pos = strrpos($category, '\\')) !== false) { + $messageFile .= (substr($category, $pos) . '.php'); + } else { + $messageFile .= "$category.php"; + } if (is_file($messageFile)) { $messages = include($messageFile); if (!is_array($messages)) { @@ -53,7 +72,7 @@ class PhpMessageSource extends MessageSource } return $messages; } else { - Yii::error("Message file not found: $messageFile", __CLASS__); + Yii::error("The message file for category '$category' does not exist: $messageFile", __CLASS__); return array(); } } diff --git a/framework/logging/Target.php b/framework/logging/Target.php index 32d12d8..b88e78d 100644 --- a/framework/logging/Target.php +++ b/framework/logging/Target.php @@ -192,8 +192,7 @@ abstract class Target extends \yii\base\Component $matched = empty($this->categories); foreach ($this->categories as $category) { - $prefix = rtrim($category, '*'); - if (strpos($message[2], $prefix) === 0 && ($message[2] === $category || $prefix !== $category)) { + if ($message[2] === $category || substr($category, -1) === '*' && strpos($message[2], rtrim($category, '*')) === 0) { $matched = true; break; } diff --git a/framework/widgets/FragmentCache.php b/framework/widgets/FragmentCache.php new file mode 100644 index 0000000..e6805f5 --- /dev/null +++ b/framework/widgets/FragmentCache.php @@ -0,0 +1,294 @@ + + * @since 2.0 + */ +class FragmentCache extends Widget +{ + /** + * Prefix to the keys for storing cached data + */ + const CACHE_KEY_PREFIX = 'Yii.COutputCache.'; + + /** + * @var string the ID of the cache application component. Defaults to 'cache' (the primary cache application component.) + */ + public $cacheID = 'cache'; + /** + * @var integer number of seconds that the data can remain in cache. Defaults to 60 seconds. + * If it is 0, existing cached content would be removed from the cache. + * If it is a negative value, the cache will be disabled (any existing cached content will + * remain in the cache.) + * + * Note, if cache dependency changes or cache space is limited, + * the data may be purged out of cache earlier. + */ + public $duration = 60; + /** + * @var mixed the dependency that the cached content depends on. + * This can be either an object implementing {@link ICacheDependency} interface or an array + * specifying the configuration of the dependency object. For example, + *
              +	 * array(
              +	 *     'class'=>'CDbCacheDependency',
              +	 *     'sql'=>'SELECT MAX(lastModified) FROM Post',
              +	 * )
              +	 * 
              + * would make the output cache depends on the last modified time of all posts. + * If any post has its modification time changed, the cached content would be invalidated. + */ + public $dependency; + /** + * @var boolean whether the content being cached should be differentiated according to route. + * A route consists of the requested controller ID and action ID. + * Defaults to true. + */ + public $varyByRoute = true; + /** + * @var boolean whether the content being cached should be differentiated according to user's language. + * A language is retrieved via Yii::app()->language. + * Defaults to false. + * @since 1.1.14 + */ + public $varyByLanguage = false; + /** + * @var array list of GET parameters that should participate in cache key calculation. + * By setting this property, the output cache will use different cached data + * for each different set of GET parameter values. + */ + public $varyByParam; + /** + * @var string a PHP expression whose result is used in the cache key calculation. + * By setting this property, the output cache will use different cached data + * for each different expression result. + * The expression can also be a valid PHP callback, + * including class method name (array(ClassName/Object, MethodName)), + * or anonymous function (PHP 5.3.0+). The function/method signature should be as follows: + *
              +	 * function foo($cache) { ... }
              +	 * 
              + * where $cache refers to the output cache component. + */ + public $varyByExpression; + /** + * @var array list of request types (e.g. GET, POST) for which the cache should be enabled only. + * Defaults to null, meaning all request types. + */ + public $requestTypes; + + private $_key; + private $_cache; + private $_contentCached; + private $_content; + private $_actions; + + /** + * Marks the start of content to be cached. + * Content displayed after this method call and before {@link endCache()} + * will be captured and saved in cache. + * This method does nothing if valid content is already found in cache. + */ + public function init() + { + if ($this->getIsContentCached()) { + $this->replayActions(); + } elseif ($this->_cache !== null) { + $this->getController()->getCachingStack()->push($this); + ob_start(); + ob_implicit_flush(false); + } + } + + /** + * Marks the end of content to be cached. + * Content displayed before this method call and after {@link init()} + * will be captured and saved in cache. + * This method does nothing if valid content is already found in cache. + */ + public function run() + { + if ($this->getIsContentCached()) { + if ($this->getController()->isCachingStackEmpty()) { + echo $this->getController()->processDynamicOutput($this->_content); + } else { + echo $this->_content; + } + } elseif ($this->_cache !== null) { + $this->_content = ob_get_clean(); + $this->getController()->getCachingStack()->pop(); + $data = array($this->_content, $this->_actions); + if (is_array($this->dependency)) { + $this->dependency = Yii::createComponent($this->dependency); + } + $this->_cache->set($this->getCacheKey(), $data, $this->duration, $this->dependency); + + if ($this->getController()->isCachingStackEmpty()) { + echo $this->getController()->processDynamicOutput($this->_content); + } else { + echo $this->_content; + } + } + } + + /** + * @return boolean whether the content can be found from cache + */ + public function getIsContentCached() + { + if ($this->_contentCached !== null) { + return $this->_contentCached; + } else { + return $this->_contentCached = $this->checkContentCache(); + } + } + + /** + * Looks for content in cache. + * @return boolean whether the content is found in cache. + */ + protected function checkContentCache() + { + if ((empty($this->requestTypes) || in_array(Yii::app()->getRequest()->getRequestType(), $this->requestTypes)) + && ($this->_cache = $this->getCache()) !== null + ) { + if ($this->duration > 0 && ($data = $this->_cache->get($this->getCacheKey())) !== false) { + $this->_content = $data[0]; + $this->_actions = $data[1]; + return true; + } + if ($this->duration == 0) { + $this->_cache->delete($this->getCacheKey()); + } + if ($this->duration <= 0) { + $this->_cache = null; + } + } + return false; + } + + /** + * @return ICache the cache used for caching the content. + */ + protected function getCache() + { + return Yii::app()->getComponent($this->cacheID); + } + + /** + * Caclulates the base cache key. + * The calculated key will be further variated in {@link getCacheKey}. + * Derived classes may override this method if more variations are needed. + * @return string basic cache key without variations + */ + protected function getBaseCacheKey() + { + return self::CACHE_KEY_PREFIX . $this->getId() . '.'; + } + + /** + * Calculates the cache key. + * The key is calculated based on {@link getBaseCacheKey} and other factors, including + * {@link varyByRoute}, {@link varyByParam}, {@link varyBySession} and {@link varyByLanguage}. + * @return string cache key + */ + protected function getCacheKey() + { + if ($this->_key !== null) { + return $this->_key; + } else { + $key = $this->getBaseCacheKey() . '.'; + if ($this->varyByRoute) { + $controller = $this->getController(); + $key .= $controller->getUniqueId() . '/'; + if (($action = $controller->getAction()) !== null) { + $key .= $action->getId(); + } + } + $key .= '.'; + + if ($this->varyBySession) { + $key .= Yii::app()->getSession()->getSessionID(); + } + $key .= '.'; + + if (is_array($this->varyByParam) && isset($this->varyByParam[0])) { + $params = array(); + foreach ($this->varyByParam as $name) { + if (isset($_GET[$name])) { + $params[$name] = $_GET[$name]; + } else { + $params[$name] = ''; + } + } + $key .= serialize($params); + } + $key .= '.'; + + if ($this->varyByExpression !== null) { + $key .= $this->evaluateExpression($this->varyByExpression); + } + $key .= '.'; + + if ($this->varyByLanguage) { + $key .= Yii::app()->language; + } + $key .= '.'; + + return $this->_key = $key; + } + } + + /** + * Records a method call when this output cache is in effect. + * When the content is served from the output cache, the recorded + * method will be re-invoked. + * @param string $context a property name of the controller. The property should refer to an object + * whose method is being recorded. If empty it means the controller itself. + * @param string $method the method name + * @param array $params parameters passed to the method + */ + public function recordAction($context, $method, $params) + { + $this->_actions[] = array($context, $method, $params); + } + + /** + * Replays the recorded method calls. + */ + protected function replayActions() + { + if (empty($this->_actions)) { + return; + } + $controller = $this->getController(); + $cs = Yii::app()->getClientScript(); + foreach ($this->_actions as $action) { + if ($action[0] === 'clientScript') { + $object = $cs; + } elseif ($action[0] === '') { + $object = $controller; + } else { + $object = $controller->{$action[0]}; + } + if (method_exists($object, $action[1])) { + call_user_func_array(array($object, $action[1]), $action[2]); + } elseif ($action[0] === '' && function_exists($action[1])) { + call_user_func_array($action[1], $action[2]); + } else { + throw new CException(Yii::t('yii', 'Unable to replay the action "{object}.{method}". The method does not exist.', + array('object' => $action[0], + 'method' => $action[1]))); + } + } + } +} \ No newline at end of file diff --git a/todo.md b/todo.md index 4d5343a..f102f41 100644 --- a/todo.md +++ b/todo.md @@ -39,7 +39,6 @@ memo * consider to be released as a separate tool for user app docs - i18n * consider using PHP built-in support and data - * message translations, choice format * formatting: number and date * parsing?? * make dates/date patterns uniform application-wide including JUI, formats etc. From 392b293d33e428a3e7540b9dbc1313a37928e528 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Mon, 25 Mar 2013 14:27:15 -0400 Subject: [PATCH 79/84] FragmentCache WIP --- framework/base/ActionFilter.php | 12 +- framework/base/View.php | 5 +- framework/caching/Cache.php | 18 +-- framework/caching/DbDependency.php | 37 ++--- framework/db/Command.php | 8 +- framework/db/Schema.php | 7 +- framework/helpers/ConsoleColor.php | 8 - framework/web/CacheSession.php | 3 +- framework/widgets/FragmentCache.php | 282 ++++++++++-------------------------- 9 files changed, 138 insertions(+), 242 deletions(-) diff --git a/framework/base/ActionFilter.php b/framework/base/ActionFilter.php index 2655c5a..0ae7ab8 100644 --- a/framework/base/ActionFilter.php +++ b/framework/base/ActionFilter.php @@ -11,9 +11,19 @@ namespace yii\base; * @author Qiang Xue * @since 2.0 */ -class Filter extends Behavior +class ActionFilter extends Behavior { /** + * @var array list of action IDs that this filter should apply to. If this property is not set, + * then the filter applies to all actions, unless they are listed in [[except]]. + */ + public $only; + /** + * @var array list of action IDs that this filter should not apply to. + */ + public $except = array(); + + /** * Declares event handlers for the [[owner]]'s events. * @return array events (array keys) and the corresponding event handler methods (array values). */ diff --git a/framework/base/View.php b/framework/base/View.php index baa1d10..18748f2 100644 --- a/framework/base/View.php +++ b/framework/base/View.php @@ -337,15 +337,16 @@ class View extends Component * ~~~ * * @param string $id a unique ID identifying the fragment to be cached. - * @param array $properties initial property values for [[\yii\widgets\OutputCache]] + * @param array $properties initial property values for [[\yii\widgets\FragmentCache]] * @return boolean whether you should generate the content for caching. * False if the cached version is available. */ public function beginCache($id, $properties = array()) { $properties['id'] = $id; + /** @var $cache \yii\widgets\FragmentCache */ $cache = $this->beginWidget('yii\widgets\OutputCache', $properties); - if ($cache->getIsContentCached()) { + if ($cache->getCachedContent() !== false) { $this->endCache(); return false; } else { diff --git a/framework/caching/Cache.php b/framework/caching/Cache.php index f3f1bc0..70cf8cb 100644 --- a/framework/caching/Cache.php +++ b/framework/caching/Cache.php @@ -8,6 +8,7 @@ namespace yii\caching; use yii\base\Component; +use yii\helpers\StringHelper; /** * Cache is the base class for cache classes supporting different cache storage implementation. @@ -70,13 +71,13 @@ abstract class Cache extends Component implements \ArrayAccess /** - * Builds a normalized cache key from one or multiple parameters. + * Builds a normalized cache key from a given key. * * The generated key contains letters and digits only, and its length is no more than 32. * - * If only one parameter is given and it is already a normalized key, then - * it will be returned back without change. Otherwise, a normalized key - * is generated by serializing all given parameters and applying MD5 hashing. + * If the given key is a string containing alphanumeric characters only and no more than 32 characters, + * then the key will be returned back without change. Otherwise, a normalized key + * is generated by serializing the given key and applying MD5 hashing. * * The following example builds a cache key using three parameters: * @@ -84,16 +85,15 @@ abstract class Cache extends Component implements \ArrayAccess * $key = $cache->buildKey($className, $method, $id); * ~~~ * - * @param string $key the first parameter + * @param array|string $key the key to be normalized * @return string the generated cache key */ public function buildKey($key) { - if (func_num_args() === 1 && ctype_alnum($key) && strlen($key) <= 32) { - return (string)$key; + if (is_string($key)) { + return ctype_alnum($key) && StringHelper::strlen($key) <= 32 ? $key : md5($key); } else { - $params = func_get_args(); - return md5(json_encode($params)); + return md5(json_encode($key)); } } diff --git a/framework/caching/DbDependency.php b/framework/caching/DbDependency.php index 9c6e1f1..247109b 100644 --- a/framework/caching/DbDependency.php +++ b/framework/caching/DbDependency.php @@ -7,15 +7,15 @@ namespace yii\caching; +use Yii; use yii\base\InvalidConfigException; use yii\db\Connection; -use yii\db\Query; /** * DbDependency represents a dependency based on the query result of a SQL statement. * * If the query result changes, the dependency is considered as changed. - * The query is specified via the [[query]] property. + * The query is specified via the [[sql]] property. * * @author Qiang Xue * @since 2.0 @@ -27,23 +27,25 @@ class DbDependency extends Dependency */ public $connectionID = 'db'; /** - * @var Query the SQL query whose result is used to determine if the dependency has been changed. + * @var string the SQL query whose result is used to determine if the dependency has been changed. * Only the first row of the query result will be used. */ - public $query; + public $sql; /** - * @var Connection the DB connection instance + * @var array the parameters (name=>value) to be bound to the SQL statement specified by [[sql]]. */ - private $_db; + public $params; /** * Constructor. - * @param Query $query the SQL query whose result is used to determine if the dependency has been changed. + * @param string $sql the SQL query whose result is used to determine if the dependency has been changed. + * @param array $params the parameters (name=>value) to be bound to the SQL statement specified by [[sql]]. * @param array $config name-value pairs that will be used to initialize the object properties */ - public function __construct($query = null, $config = array()) + public function __construct($sql, $params = array(), $config = array()) { - $this->query = $query; + $this->sql = $sql; + $this->params = $params; parent::__construct($config); } @@ -66,22 +68,23 @@ class DbDependency extends Dependency protected function generateDependencyData() { $db = $this->getDb(); - /** - * @var \yii\db\Command $command - */ - $command = $this->query->createCommand($db); if ($db->enableQueryCache) { // temporarily disable and re-enable query caching $db->enableQueryCache = false; - $result = $command->queryRow(); + $result = $db->createCommand($this->sql, $this->params)->queryRow(); $db->enableQueryCache = true; } else { - $result = $command->queryRow(); + $result = $db->createCommand($this->sql, $this->params)->queryRow(); } return $result; } /** + * @var Connection the DB connection instance + */ + private $_db; + + /** * Returns the DB connection instance used for caching purpose. * @return Connection the DB connection instance * @throws InvalidConfigException if [[connectionID]] does not point to a valid application component. @@ -89,11 +92,11 @@ class DbDependency extends Dependency public function getDb() { if ($this->_db === null) { - $db = \Yii::$app->getComponent($this->connectionID); + $db = Yii::$app->getComponent($this->connectionID); if ($db instanceof Connection) { $this->_db = $db; } else { - throw new InvalidConfigException("DbCache::connectionID must refer to the ID of a DB application component."); + throw new InvalidConfigException("DbCacheDependency::connectionID must refer to the ID of a DB application component."); } } return $this->_db; diff --git a/framework/db/Command.php b/framework/db/Command.php index 0861b8d..c2b2e05 100644 --- a/framework/db/Command.php +++ b/framework/db/Command.php @@ -389,7 +389,13 @@ class Command extends \yii\base\Component } if (isset($cache)) { - $cacheKey = $cache->buildKey(__CLASS__, $db->dsn, $db->username, $sql, $paramLog); + $cacheKey = $cache->buildKey(array( + __CLASS__, + $db->dsn, + $db->username, + $sql, + $paramLog, + )); if (($result = $cache->get($cacheKey)) !== false) { \Yii::trace('Query result found in cache', __CLASS__); return $result; diff --git a/framework/db/Schema.php b/framework/db/Schema.php index 5a6ffda..5fe6121 100644 --- a/framework/db/Schema.php +++ b/framework/db/Schema.php @@ -109,7 +109,12 @@ abstract class Schema extends \yii\base\Object */ public function getCacheKey($cache, $name) { - return $cache->buildKey(__CLASS__, $this->db->dsn, $this->db->username, $name); + return $cache->buildKey(array( + __CLASS__, + $this->db->dsn, + $this->db->username, + $name, + )); } /** diff --git a/framework/helpers/ConsoleColor.php b/framework/helpers/ConsoleColor.php index c64db24..429aeb1 100644 --- a/framework/helpers/ConsoleColor.php +++ b/framework/helpers/ConsoleColor.php @@ -7,15 +7,7 @@ namespace yii\helpers; -// todo define how subclassing will work -// todo add a run() or render() method // todo test this on all kinds of terminals, especially windows (check out lib ncurses) -// todo not sure if all methods should be static - -// todo subclass DetailView -// todo subclass GridView -// todo more subclasses - /** * Console View is the base class for console view components diff --git a/framework/web/CacheSession.php b/framework/web/CacheSession.php index 25524a0..d7882a6 100644 --- a/framework/web/CacheSession.php +++ b/framework/web/CacheSession.php @@ -45,6 +45,7 @@ class CacheSession extends Session { return true; } + /** * Returns the cache instance used for storing session data. * @return Cache the cache instance @@ -114,6 +115,6 @@ class CacheSession extends Session */ protected function calculateKey($id) { - return $this->getCache()->buildKey(__CLASS__, $id); + return $this->getCache()->buildKey(array(__CLASS__, $id)); } } diff --git a/framework/widgets/FragmentCache.php b/framework/widgets/FragmentCache.php index e6805f5..38073f0 100644 --- a/framework/widgets/FragmentCache.php +++ b/framework/widgets/FragmentCache.php @@ -7,7 +7,11 @@ namespace yii\widgets; +use Yii; +use yii\base\InvalidConfigException; use yii\base\Widget; +use yii\caching\Cache; +use yii\caching\Dependency; /** * @author Qiang Xue @@ -16,81 +20,47 @@ use yii\base\Widget; class FragmentCache extends Widget { /** - * Prefix to the keys for storing cached data - */ - const CACHE_KEY_PREFIX = 'Yii.COutputCache.'; - - /** * @var string the ID of the cache application component. Defaults to 'cache' (the primary cache application component.) */ public $cacheID = 'cache'; /** - * @var integer number of seconds that the data can remain in cache. Defaults to 60 seconds. - * If it is 0, existing cached content would be removed from the cache. - * If it is a negative value, the cache will be disabled (any existing cached content will - * remain in the cache.) - * - * Note, if cache dependency changes or cache space is limited, - * the data may be purged out of cache earlier. + * @var integer number of seconds that the data can remain valid in cache. + * Use 0 to indicate that the cached data will never expire. */ public $duration = 60; /** - * @var mixed the dependency that the cached content depends on. - * This can be either an object implementing {@link ICacheDependency} interface or an array - * specifying the configuration of the dependency object. For example, - *
              +	 * @var array|Dependency the dependency that the cached content depends on.
              +	 * This can be either a [[Dependency]] object or a configuration array for creating the dependency object.
              +	 * For example,
              +	 *
              +	 * ~~~
               	 * array(
              -	 *     'class'=>'CDbCacheDependency',
              -	 *     'sql'=>'SELECT MAX(lastModified) FROM Post',
              +	 *     'class' => 'yii\caching\DbDependency',
              +	 *     'sql' => 'SELECT MAX(lastModified) FROM Post',
               	 * )
              -	 * 
              + * ~~~ + * * would make the output cache depends on the last modified time of all posts. * If any post has its modification time changed, the cached content would be invalidated. */ public $dependency; /** - * @var boolean whether the content being cached should be differentiated according to route. - * A route consists of the requested controller ID and action ID. - * Defaults to true. - */ - public $varyByRoute = true; - /** - * @var boolean whether the content being cached should be differentiated according to user's language. - * A language is retrieved via Yii::app()->language. - * Defaults to false. - * @since 1.1.14 - */ - public $varyByLanguage = false; - /** - * @var array list of GET parameters that should participate in cache key calculation. - * By setting this property, the output cache will use different cached data - * for each different set of GET parameter values. - */ - public $varyByParam; - /** - * @var string a PHP expression whose result is used in the cache key calculation. - * By setting this property, the output cache will use different cached data - * for each different expression result. - * The expression can also be a valid PHP callback, - * including class method name (array(ClassName/Object, MethodName)), - * or anonymous function (PHP 5.3.0+). The function/method signature should be as follows: - *
              -	 * function foo($cache) { ... }
              -	 * 
              - * where $cache refers to the output cache component. + * @var array list of factors that would cause the variation of the content being cached. + * Each factor is a string representing a variation (e.g. the language, a GET parameter). + * The following variation setting will cause the content to be cached in different versions + * according to the current application language: + * + * ~~~ + * array( + * Yii::$app->language, + * ) */ - public $varyByExpression; + public $variations; /** - * @var array list of request types (e.g. GET, POST) for which the cache should be enabled only. - * Defaults to null, meaning all request types. + * @var boolean whether to enable the fragment cache. You may use this property to turn on and off + * the fragment cache according to specific setting (e.g. enable fragment cache only for GET requests). */ - public $requestTypes; - - private $_key; - private $_cache; - private $_contentCached; - private $_content; - private $_actions; + public $enabled = true; /** * Marks the start of content to be cached. @@ -100,10 +70,7 @@ class FragmentCache extends Widget */ public function init() { - if ($this->getIsContentCached()) { - $this->replayActions(); - } elseif ($this->_cache !== null) { - $this->getController()->getCachingStack()->push($this); + if ($this->getCachedContent() === false && $this->getCache() !== null) { ob_start(); ob_implicit_flush(false); } @@ -117,178 +84,89 @@ class FragmentCache extends Widget */ public function run() { - if ($this->getIsContentCached()) { - if ($this->getController()->isCachingStackEmpty()) { - echo $this->getController()->processDynamicOutput($this->_content); - } else { - echo $this->_content; - } - } elseif ($this->_cache !== null) { - $this->_content = ob_get_clean(); - $this->getController()->getCachingStack()->pop(); - $data = array($this->_content, $this->_actions); + if (($content = $this->getCachedContent()) !== false) { + echo $content; + } elseif (($cache = $this->getCache()) !== false) { + $content = ob_get_clean(); if (is_array($this->dependency)) { - $this->dependency = Yii::createComponent($this->dependency); - } - $this->_cache->set($this->getCacheKey(), $data, $this->duration, $this->dependency); - - if ($this->getController()->isCachingStackEmpty()) { - echo $this->getController()->processDynamicOutput($this->_content); - } else { - echo $this->_content; + $this->dependency = Yii::createObject($this->dependency); } + $cache->set($this->calculateKey(), $content, $this->duration, $this->dependency); + echo $content; } } /** - * @return boolean whether the content can be found from cache + * @var string|boolean the cached content. False if the content is not cached. */ - public function getIsContentCached() - { - if ($this->_contentCached !== null) { - return $this->_contentCached; - } else { - return $this->_contentCached = $this->checkContentCache(); - } - } + private $_content; /** - * Looks for content in cache. - * @return boolean whether the content is found in cache. + * Returns the cached content if available. + * @return string|boolean the cached content. False is returned if valid content is not found in the cache. */ - protected function checkContentCache() + public function getCachedContent() { - if ((empty($this->requestTypes) || in_array(Yii::app()->getRequest()->getRequestType(), $this->requestTypes)) - && ($this->_cache = $this->getCache()) !== null - ) { - if ($this->duration > 0 && ($data = $this->_cache->get($this->getCacheKey())) !== false) { - $this->_content = $data[0]; - $this->_actions = $data[1]; - return true; - } - if ($this->duration == 0) { - $this->_cache->delete($this->getCacheKey()); - } - if ($this->duration <= 0) { - $this->_cache = null; + if ($this->_content === null) { + if (($cache = $this->getCache()) !== null) { + $key = $this->calculateKey(); + $this->_content = $cache->get($key); + } else { + $this->_content = false; } } - return false; + return $this->_content; } /** - * @return ICache the cache used for caching the content. + * Generates a unique key used for storing the content in cache. + * The key generated depends on both [[id]] and [[variations]]. + * @return string a valid cache key */ - protected function getCache() + protected function calculateKey() { - return Yii::app()->getComponent($this->cacheID); + $factors = array(__CLASS__, $this->getId()); + if (is_array($this->variations)) { + foreach ($this->variations as $factor) { + $factors[] = $factor; + } + } + return $this->getCache()->buildKey($factors); } /** - * Caclulates the base cache key. - * The calculated key will be further variated in {@link getCacheKey}. - * Derived classes may override this method if more variations are needed. - * @return string basic cache key without variations + * @var Cache */ - protected function getBaseCacheKey() - { - return self::CACHE_KEY_PREFIX . $this->getId() . '.'; - } + private $_cache; /** - * Calculates the cache key. - * The key is calculated based on {@link getBaseCacheKey} and other factors, including - * {@link varyByRoute}, {@link varyByParam}, {@link varyBySession} and {@link varyByLanguage}. - * @return string cache key + * Returns the cache instance used for storing content. + * @return Cache the cache instance. Null is returned if the cache component is not available + * or [[enabled]] is false. + * @throws InvalidConfigException if [[cacheID]] does not point to a valid application component. */ - protected function getCacheKey() + public function getCache() { - if ($this->_key !== null) { - return $this->_key; - } else { - $key = $this->getBaseCacheKey() . '.'; - if ($this->varyByRoute) { - $controller = $this->getController(); - $key .= $controller->getUniqueId() . '/'; - if (($action = $controller->getAction()) !== null) { - $key .= $action->getId(); - } - } - $key .= '.'; - - if ($this->varyBySession) { - $key .= Yii::app()->getSession()->getSessionID(); - } - $key .= '.'; - - if (is_array($this->varyByParam) && isset($this->varyByParam[0])) { - $params = array(); - foreach ($this->varyByParam as $name) { - if (isset($_GET[$name])) { - $params[$name] = $_GET[$name]; - } else { - $params[$name] = ''; - } - } - $key .= serialize($params); - } - $key .= '.'; - - if ($this->varyByExpression !== null) { - $key .= $this->evaluateExpression($this->varyByExpression); - } - $key .= '.'; - - if ($this->varyByLanguage) { - $key .= Yii::app()->language; + if (!$this->enabled) { + return null; + } + if ($this->_cache === null) { + $cache = Yii::$app->getComponent($this->cacheID); + if ($cache instanceof Cache) { + $this->_cache = $cache; + } else { + throw new InvalidConfigException('FragmentCache::cacheID must refer to the ID of a cache application component.'); } - $key .= '.'; - - return $this->_key = $key; } + return $this->_cache; } /** - * Records a method call when this output cache is in effect. - * When the content is served from the output cache, the recorded - * method will be re-invoked. - * @param string $context a property name of the controller. The property should refer to an object - * whose method is being recorded. If empty it means the controller itself. - * @param string $method the method name - * @param array $params parameters passed to the method - */ - public function recordAction($context, $method, $params) - { - $this->_actions[] = array($context, $method, $params); - } - - /** - * Replays the recorded method calls. + * Sets the cache instance used by the session component. + * @param Cache $value the cache instance */ - protected function replayActions() + public function setCache($value) { - if (empty($this->_actions)) { - return; - } - $controller = $this->getController(); - $cs = Yii::app()->getClientScript(); - foreach ($this->_actions as $action) { - if ($action[0] === 'clientScript') { - $object = $cs; - } elseif ($action[0] === '') { - $object = $controller; - } else { - $object = $controller->{$action[0]}; - } - if (method_exists($object, $action[1])) { - call_user_func_array(array($object, $action[1]), $action[2]); - } elseif ($action[0] === '' && function_exists($action[1])) { - call_user_func_array($action[1], $action[2]); - } else { - throw new CException(Yii::t('yii', 'Unable to replay the action "{object}.{method}". The method does not exist.', - array('object' => $action[0], - 'method' => $action[1]))); - } - } + $this->_cache = $value; } } \ No newline at end of file From 2f3cc8f16ecd9bfb55e847b5245faf949ce54a84 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Mon, 25 Mar 2013 14:37:25 -0400 Subject: [PATCH 80/84] finished ActionFilter. --- framework/base/ActionFilter.php | 43 +++++++++++++++++++++++++++++++++++++---- todo.md | 17 ---------------- 2 files changed, 39 insertions(+), 21 deletions(-) diff --git a/framework/base/ActionFilter.php b/framework/base/ActionFilter.php index 0ae7ab8..1f82e5d 100644 --- a/framework/base/ActionFilter.php +++ b/framework/base/ActionFilter.php @@ -30,8 +30,8 @@ class ActionFilter extends Behavior public function events() { return array( - 'beforeAction' => 'beforeAction', - 'afterAction' => 'afterAction', + 'beforeAction' => 'beforeFilter', + 'afterAction' => 'afterFilter', ); } @@ -39,8 +39,11 @@ class ActionFilter extends Behavior * @param ActionEvent $event * @return boolean */ - public function beforeAction($event) + public function beforeFilter($event) { + if ($this->isActive($event->action)) { + $event->isValid = $this->beforeAction($event->action); + } return $event->isValid; } @@ -48,8 +51,40 @@ class ActionFilter extends Behavior * @param ActionEvent $event * @return boolean */ - public function afterAction($event) + public function afterFilter($event) { + if ($this->isActive($event->action)) { + $this->afterAction($event->action); + } + } + + /** + * This method is invoked right before an action is to be executed (after all possible filters.) + * You may override this method to do last-minute preparation for the action. + * @param Action $action the action to be executed. + * @return boolean whether the action should continue to be executed. + */ + public function beforeAction($action) + { + return true; + } + + /** + * This method is invoked right after an action is executed. + * You may override this method to do some postprocessing for the action. + * @param Action $action the action just executed. + */ + public function afterAction($action) + { + } + /** + * Returns a value indicating whether the filer is active for the given action. + * @param Action $action the action being filtered + * @return boolean whether the filer is active for the given action. + */ + protected function isActive($action) + { + return !in_array($action->id, $this->except, true) && (empty($this->only) || in_array($action->id, $this->only, true)); } } \ No newline at end of file diff --git a/todo.md b/todo.md index f102f41..f66d3c1 100644 --- a/todo.md +++ b/todo.md @@ -1,21 +1,4 @@ -- console - * If console is executed using Windows, do not use colors. If not, use colors. Allow to override via console application settings. -- db - * pgsql, sql server, oracle, db2 drivers - * unit tests on different DB drivers - * document-based (should allow storage-specific methods additionally to generic ones) - * mongodb (put it under framework/db/mongodb) - * key-value-based (should allow storage-specific methods additionally to generic ones) - * redis (put it under framework/db/redis or perhaps framework/caching?) -- base - * TwigViewRenderer (Alex) - * SmartyViewRenderer (Alex) -- logging - * WebTarget (TBD after web is in place): should consider using javascript and make it into a toolbar - * ProfileTarget (TBD after web is in place): should consider using javascript and make it into a toolbar - * unit tests - caching - * backend-specific unit tests * dependency unit tests - validators * Refactor validators to add validateValue() for every validator, if possible. Check if value is an array. From 80dbaaca8bb390b735b1384f05aad6a71acfb7c8 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Mon, 25 Mar 2013 19:37:42 -0400 Subject: [PATCH 81/84] bug fixes. --- framework/base/Application.php | 3 +++ framework/base/ErrorHandler.php | 8 ++++---- framework/views/exception.php | 2 +- framework/web/Application.php | 6 ++++++ 4 files changed, 14 insertions(+), 5 deletions(-) diff --git a/framework/base/Application.php b/framework/base/Application.php index 31087e2..fd2ecad 100644 --- a/framework/base/Application.php +++ b/framework/base/Application.php @@ -479,6 +479,9 @@ class Application extends Module $msg = (string)$e; $msg .= "\nPrevious exception:\n"; $msg .= (string)$exception; + if (YII_DEBUG) { + echo $msg; + } $msg .= "\n\$_SERVER = " . var_export($_SERVER, true); error_log($msg); exit(1); diff --git a/framework/base/ErrorHandler.php b/framework/base/ErrorHandler.php index a3ab137..f71b8c8 100644 --- a/framework/base/ErrorHandler.php +++ b/framework/base/ErrorHandler.php @@ -78,7 +78,7 @@ class ErrorHandler extends Component if (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] === 'XMLHttpRequest') { \Yii::$app->renderException($exception); } else { - $view = new View($this); + $view = new View; if (!YII_DEBUG || $exception instanceof UserException) { $viewName = $this->errorView; } else { @@ -86,7 +86,7 @@ class ErrorHandler extends Component } echo $view->render($viewName, array( 'exception' => $exception, - )); + ), $this); } } else { \Yii::$app->renderException($exception); @@ -255,8 +255,8 @@ class ErrorHandler extends Component { $view = new View; $name = !YII_DEBUG || $exception instanceof HttpException ? $this->errorView : $this->exceptionView; - echo $view->render($this, $name, array( + echo $view->render($name, array( 'exception' => $exception, - )); + ), $this); } } diff --git a/framework/views/exception.php b/framework/views/exception.php index 6257ffe..db29302 100644 --- a/framework/views/exception.php +++ b/framework/views/exception.php @@ -183,7 +183,7 @@ $title = $context->htmlEncode($exception instanceof \yii\base\Exception || $exce
              - versionInfo : ''?> + getVersionInfo() : ''?>
              diff --git a/framework/web/Application.php b/framework/web/Application.php index 61e84a3..6e0cc73 100644 --- a/framework/web/Application.php +++ b/framework/web/Application.php @@ -6,6 +6,7 @@ */ namespace yii\web; + use yii\base\InvalidParamException; /** @@ -17,6 +18,11 @@ use yii\base\InvalidParamException; class Application extends \yii\base\Application { /** + * @var string the default route of this application. Defaults to 'site'. + */ + public $defaultRoute = 'site'; + + /** * Sets default path aliases. */ public function registerDefaultAliases() From 27f5c49bd5e0dd49ce1f53a64c986a44823eb702 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Mon, 25 Mar 2013 19:39:09 -0400 Subject: [PATCH 82/84] fragment cache WIP --- framework/base/View.php | 19 +++++++++++++++++++ framework/widgets/FragmentCache.php | 33 +++++++++++++++++++++++++++++---- 2 files changed, 48 insertions(+), 4 deletions(-) diff --git a/framework/base/View.php b/framework/base/View.php index 18748f2..7170251 100644 --- a/framework/base/View.php +++ b/framework/base/View.php @@ -156,6 +156,25 @@ class View extends Component return ob_get_clean(); } + public function renderDynamic($statements) + { + if (!empty($this->cachingStack)) { + $n = count($this->_dynamicOutput); + $placeholder = ""; + foreach ($this->cachingStack as $cache) { + $cache->dynamicPlaceholders[$placeholder] = $statements; + } + return $placeholder; + } else { + return $this->evaluateDynamicContent($statements); + } + } + + public function evaluateDynamicContent($statements) + { + return eval($statements); + } + /** * Finds the view file based on the given view name. * diff --git a/framework/widgets/FragmentCache.php b/framework/widgets/FragmentCache.php index 38073f0..b56b86f 100644 --- a/framework/widgets/FragmentCache.php +++ b/framework/widgets/FragmentCache.php @@ -61,6 +61,16 @@ class FragmentCache extends Widget * the fragment cache according to specific setting (e.g. enable fragment cache only for GET requests). */ public $enabled = true; + /** + * @var \yii\base\View the view object within which this widget is sued. If not set, + * the view registered with the application will be used. This is mainly used by dynamic content feature. + */ + public $view; + /** + * @var array + */ + public $dynamicPlaceholders; + /** * Marks the start of content to be cached. @@ -70,7 +80,11 @@ class FragmentCache extends Widget */ public function init() { + if ($this->view === null) { + $this->view = Yii::$app->getView(); + } if ($this->getCachedContent() === false && $this->getCache() !== null) { + array_push($this->view->cachingStack, $this); ob_start(); ob_implicit_flush(false); } @@ -88,10 +102,12 @@ class FragmentCache extends Widget echo $content; } elseif (($cache = $this->getCache()) !== false) { $content = ob_get_clean(); + array_pop($this->view->cachingStack); if (is_array($this->dependency)) { $this->dependency = Yii::createObject($this->dependency); } - $cache->set($this->calculateKey(), $content, $this->duration, $this->dependency); + $data = array($content, $this->dynamicPlaceholders); + $cache->set($this->calculateKey(), $data, $this->duration, $this->dependency); echo $content; } } @@ -108,11 +124,20 @@ class FragmentCache extends Widget public function getCachedContent() { if ($this->_content === null) { + $this->_content = false; if (($cache = $this->getCache()) !== null) { $key = $this->calculateKey(); - $this->_content = $cache->get($key); - } else { - $this->_content = false; + $data = $cache->get($key); + if (is_array($data) && count($data) === 2) { + list ($content, $placeholders) = $data; + if (is_array($placeholders) && count($placeholders) > 0) { + foreach ($placeholders as $name => $statements) { + $placeholders[$name] = $this->view->evaluateDynamicContent($statements); + } + $content = strtr($content, $placeholders); + } + $this->_content = $content; + } } } return $this->_content; From de5c304f64426d0585205469fdf5d0a9a386ecac Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Mon, 25 Mar 2013 22:26:25 -0400 Subject: [PATCH 83/84] finished fragment caching. --- framework/YiiBase.php | 2 +- framework/base/Component.php | 6 ++-- framework/base/Object.php | 8 ++--- framework/base/View.php | 60 ++++++++++++++++++++++++++++++------- framework/widgets/FragmentCache.php | 30 ++++++++++++++----- 5 files changed, 80 insertions(+), 26 deletions(-) diff --git a/framework/YiiBase.php b/framework/YiiBase.php index 678856c..16e237d 100644 --- a/framework/YiiBase.php +++ b/framework/YiiBase.php @@ -298,7 +298,7 @@ class YiiBase } } - if (isset($classFile, $alias)) { + if (isset($classFile, $alias) && is_file($classFile)) { if (!YII_DEBUG || basename(realpath($classFile)) === basename($alias) . '.php') { include($classFile); return true; diff --git a/framework/base/Component.php b/framework/base/Component.php index 515f7e1..2d081d3 100644 --- a/framework/base/Component.php +++ b/framework/base/Component.php @@ -58,7 +58,7 @@ class Component extends \yii\base\Object } } } - throw new UnknownPropertyException('Getting unknown property: ' . get_class($this) . '.' . $name); + throw new UnknownPropertyException('Getting unknown property: ' . get_class($this) . '::' . $name); } /** @@ -105,9 +105,9 @@ class Component extends \yii\base\Object } } if (method_exists($this, 'get' . $name)) { - throw new InvalidCallException('Setting read-only property: ' . get_class($this) . '.' . $name); + throw new InvalidCallException('Setting read-only property: ' . get_class($this) . '::' . $name); } else { - throw new UnknownPropertyException('Setting unknown property: ' . get_class($this) . '.' . $name); + throw new UnknownPropertyException('Setting unknown property: ' . get_class($this) . '::' . $name); } } diff --git a/framework/base/Object.php b/framework/base/Object.php index f589e38..3bd8378 100644 --- a/framework/base/Object.php +++ b/framework/base/Object.php @@ -65,7 +65,7 @@ class Object if (method_exists($this, $getter)) { return $this->$getter(); } else { - throw new UnknownPropertyException('Getting unknown property: ' . get_class($this) . '.' . $name); + throw new UnknownPropertyException('Getting unknown property: ' . get_class($this) . '::' . $name); } } @@ -86,9 +86,9 @@ class Object if (method_exists($this, $setter)) { $this->$setter($value); } elseif (method_exists($this, 'get' . $name)) { - throw new InvalidCallException('Setting read-only property: ' . get_class($this) . '.' . $name); + throw new InvalidCallException('Setting read-only property: ' . get_class($this) . '::' . $name); } else { - throw new UnknownPropertyException('Setting unknown property: ' . get_class($this) . '.' . $name); + throw new UnknownPropertyException('Setting unknown property: ' . get_class($this) . '::' . $name); } } @@ -129,7 +129,7 @@ class Object if (method_exists($this, $setter)) { $this->$setter(null); } elseif (method_exists($this, 'get' . $name)) { - throw new InvalidCallException('Unsetting read-only property: ' . get_class($this) . '.' . $name); + throw new InvalidCallException('Unsetting read-only property: ' . get_class($this) . '::' . $name); } } diff --git a/framework/base/View.php b/framework/base/View.php index 7170251..c7087c1 100644 --- a/framework/base/View.php +++ b/framework/base/View.php @@ -45,11 +45,21 @@ class View extends Component * through this property. */ public $clips; - /** - * @var Widget[] the widgets that are currently not ended + * @var Widget[] the widgets that are currently being rendered (not ended). This property + * is maintained by [[beginWidget()]] and [[endWidget()]] methods. Do not modify it directly. + */ + public $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. + */ + 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. */ - private $_widgetStack = array(); + public $dynamicPlaceholders = array(); /** @@ -156,20 +166,47 @@ class View extends Component return ob_get_clean(); } + /** + * Renders dynamic content returned by the given PHP statements. + * This method is mainly used together with content caching (fragment caching and page caching) + * when some portions of the content (called *dynamic content*) should not be cached. + * The dynamic content must be returned by some PHP statements. + * @param string $statements the PHP statements for generating the dynamic content. + * @return string the placeholder of the dynamic content, or the dynamic content if there is no + * active content cache currently. + */ public function renderDynamic($statements) { - if (!empty($this->cachingStack)) { - $n = count($this->_dynamicOutput); + if (!empty($this->cacheStack)) { + $n = count($this->dynamicPlaceholders); $placeholder = ""; - foreach ($this->cachingStack as $cache) { - $cache->dynamicPlaceholders[$placeholder] = $statements; - } + $this->addDynamicPlaceholder($placeholder, $statements); return $placeholder; } else { return $this->evaluateDynamicContent($statements); } } + /** + * Adds a placeholder for dynamic content. + * This method is internally used. + * @param string $placeholder the placeholder name + * @param string $statements the PHP statements for generating the dynamic content + */ + public function addDynamicPlaceholder($placeholder, $statements) + { + foreach ($this->cacheStack as $cache) { + $cache->dynamicPlaceholders[$placeholder] = $statements; + } + $this->dynamicPlaceholders[$placeholder] = $statements; + } + + /** + * Evaluates the given PHP statements. + * This method is mainly used internally to implement dynamic content feature. + * @param string $statements the PHP statements to be evaluated. + * @return mixed the return value of the PHP statements. + */ public function evaluateDynamicContent($statements) { return eval($statements); @@ -267,7 +304,7 @@ class View extends Component public function beginWidget($class, $properties = array()) { $widget = $this->createWidget($class, $properties); - $this->_widgetStack[] = $widget; + $this->widgetStack[] = $widget; return $widget; } @@ -281,7 +318,7 @@ class View extends Component */ public function endWidget() { - $widget = array_pop($this->_widgetStack); + $widget = array_pop($this->widgetStack); if ($widget instanceof Widget) { $widget->run(); return $widget; @@ -363,8 +400,9 @@ class View extends Component public function beginCache($id, $properties = array()) { $properties['id'] = $id; + $properties['view'] = $this; /** @var $cache \yii\widgets\FragmentCache */ - $cache = $this->beginWidget('yii\widgets\OutputCache', $properties); + $cache = $this->beginWidget('yii\widgets\FragmentCache', $properties); if ($cache->getCachedContent() !== false) { $this->endCache(); return false; diff --git a/framework/widgets/FragmentCache.php b/framework/widgets/FragmentCache.php index b56b86f..3bb321e 100644 --- a/framework/widgets/FragmentCache.php +++ b/framework/widgets/FragmentCache.php @@ -67,7 +67,8 @@ class FragmentCache extends Widget */ public $view; /** - * @var 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. */ public $dynamicPlaceholders; @@ -83,8 +84,8 @@ class FragmentCache extends Widget if ($this->view === null) { $this->view = Yii::$app->getView(); } - if ($this->getCachedContent() === false && $this->getCache() !== null) { - array_push($this->view->cachingStack, $this); + if ($this->getCache() !== null && $this->getCachedContent() === false) { + $this->view->cacheStack[] = $this; ob_start(); ob_implicit_flush(false); } @@ -100,14 +101,18 @@ class FragmentCache extends Widget { if (($content = $this->getCachedContent()) !== false) { echo $content; - } elseif (($cache = $this->getCache()) !== false) { + } elseif (($cache = $this->getCache()) !== null) { $content = ob_get_clean(); - array_pop($this->view->cachingStack); + array_pop($this->view->cacheStack); if (is_array($this->dependency)) { $this->dependency = Yii::createObject($this->dependency); } $data = array($content, $this->dynamicPlaceholders); $cache->set($this->calculateKey(), $data, $this->duration, $this->dependency); + + if ($this->view->cacheStack === array() && !empty($this->dynamicPlaceholders)) { + $content = $this->updateDynamicContent($content, $this->dynamicPlaceholders); + } echo $content; } } @@ -131,10 +136,13 @@ class FragmentCache extends Widget if (is_array($data) && count($data) === 2) { list ($content, $placeholders) = $data; if (is_array($placeholders) && count($placeholders) > 0) { + if ($this->view->cacheStack === array()) { + // outermost cache: replace placeholder with dynamic content + $content = $this->updateDynamicContent($content, $placeholders); + } foreach ($placeholders as $name => $statements) { - $placeholders[$name] = $this->view->evaluateDynamicContent($statements); + $this->view->addDynamicPlaceholder($name, $statements); } - $content = strtr($content, $placeholders); } $this->_content = $content; } @@ -143,6 +151,14 @@ class FragmentCache extends Widget return $this->_content; } + protected function updateDynamicContent($content, $placeholders) + { + foreach ($placeholders as $name => $statements) { + $placeholders[$name] = $this->view->evaluateDynamicContent($statements); + } + return strtr($content, $placeholders); + } + /** * Generates a unique key used for storing the content in cache. * The key generated depends on both [[id]] and [[variations]]. From 0ed14a741d2c13f66e2023823975287694845621 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Mon, 25 Mar 2013 23:11:32 -0400 Subject: [PATCH 84/84] Finished PageCache. --- framework/web/PageCache.php | 110 ++++++++++++++++++++++++++++++++++++ framework/widgets/FragmentCache.php | 2 +- 2 files changed, 111 insertions(+), 1 deletion(-) create mode 100644 framework/web/PageCache.php diff --git a/framework/web/PageCache.php b/framework/web/PageCache.php new file mode 100644 index 0000000..24cddea --- /dev/null +++ b/framework/web/PageCache.php @@ -0,0 +1,110 @@ + + * @since 2.0 + */ +class PageCache extends ActionFilter +{ + /** + * @var boolean whether the content being cached should be differentiated according to the route. + * A route consists of the requested controller ID and action ID. Defaults to true. + */ + public $varyByRoute = true; + /** + * @var View the view object that is used to create the fragment cache widget to implement page caching. + * If not set, the view registered with the application will be used. + */ + public $view; + + /** + * @var string the ID of the cache application component. Defaults to 'cache' (the primary cache application component.) + */ + public $cacheID = 'cache'; + /** + * @var integer number of seconds that the data can remain valid in cache. + * Use 0 to indicate that the cached data will never expire. + */ + public $duration = 60; + /** + * @var array|Dependency the dependency that the cached content depends on. + * This can be either a [[Dependency]] object or a configuration array for creating the dependency object. + * For example, + * + * ~~~ + * array( + * 'class' => 'yii\caching\DbDependency', + * 'sql' => 'SELECT MAX(lastModified) FROM Post', + * ) + * ~~~ + * + * would make the output cache depends on the last modified time of all posts. + * If any post has its modification time changed, the cached content would be invalidated. + */ + public $dependency; + /** + * @var array list of factors that would cause the variation of the content being cached. + * Each factor is a string representing a variation (e.g. the language, a GET parameter). + * The following variation setting will cause the content to be cached in different versions + * according to the current application language: + * + * ~~~ + * array( + * Yii::$app->language, + * ) + */ + public $variations; + /** + * @var boolean whether to enable the fragment cache. You may use this property to turn on and off + * the fragment cache according to specific setting (e.g. enable fragment cache only for GET requests). + */ + public $enabled = true; + + + public function init() + { + parent::init(); + if ($this->view === null) { + $this->view = Yii::$app->getView(); + } + } + + /** + * This method is invoked right before an action is to be executed (after all possible filters.) + * You may override this method to do last-minute preparation for the action. + * @param Action $action the action to be executed. + * @return boolean whether the action should continue to be executed. + */ + public function beforeAction($action) + { + $properties = array(); + foreach (array('cacheID', 'duration', 'dependency', 'variations', 'enabled') as $name) { + $properties[$name] = $this->$name; + } + $id = $this->varyByRoute ? $action->getUniqueId() : __CLASS__; + return $this->view->beginCache($id, $properties); + } + + /** + * This method is invoked right after an action is executed. + * You may override this method to do some postprocessing for the action. + * @param Action $action the action just executed. + */ + public function afterAction($action) + { + $this->view->endCache(); + } +} \ No newline at end of file diff --git a/framework/widgets/FragmentCache.php b/framework/widgets/FragmentCache.php index 3bb321e..d5185f8 100644 --- a/framework/widgets/FragmentCache.php +++ b/framework/widgets/FragmentCache.php @@ -62,7 +62,7 @@ class FragmentCache extends Widget */ public $enabled = true; /** - * @var \yii\base\View the view object within which this widget is sued. If not set, + * @var \yii\base\View the view object within which this widget is used. If not set, * the view registered with the application will be used. This is mainly used by dynamic content feature. */ public $view;