From e7295ad564a327397e0807f04109d12643ceaca2 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Thu, 28 Mar 2013 20:07:49 -0400 Subject: [PATCH 001/104] Use __METHOD__ as log category. --- framework/base/Model.php | 2 +- framework/base/Module.php | 4 ++-- framework/db/Command.php | 28 +++++++++++++++------------- framework/db/Connection.php | 6 +++--- framework/db/Transaction.php | 6 +++--- framework/i18n/PhpMessageSource.php | 2 +- framework/web/Identity.php | 7 +++---- framework/web/Session.php | 2 +- framework/web/User.php | 11 ++++++++++- 9 files changed, 39 insertions(+), 29 deletions(-) diff --git a/framework/base/Model.php b/framework/base/Model.php index 7818293..b9b5846 100644 --- a/framework/base/Model.php +++ b/framework/base/Model.php @@ -541,7 +541,7 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess public function onUnsafeAttribute($name, $value) { if (YII_DEBUG) { - \Yii::info("Failed to set unsafe attribute '$name' in '" . get_class($this) . "'.", __CLASS__); + \Yii::info("Failed to set unsafe attribute '$name' in '" . get_class($this) . "'.", __METHOD__); } } diff --git a/framework/base/Module.php b/framework/base/Module.php index 6b82157..cf751c0 100644 --- a/framework/base/Module.php +++ b/framework/base/Module.php @@ -346,7 +346,7 @@ abstract class Module extends Component if ($this->_modules[$id] instanceof Module) { return $this->_modules[$id]; } elseif ($load) { - Yii::trace("Loading module: $id", __CLASS__); + Yii::trace("Loading module: $id", __METHOD__); return $this->_modules[$id] = Yii::createObject($this->_modules[$id], $id, $this); } } @@ -452,7 +452,7 @@ abstract class Module extends Component if ($this->_components[$id] instanceof Component) { return $this->_components[$id]; } elseif ($load) { - Yii::trace("Loading component: $id", __CLASS__); + Yii::trace("Loading component: $id", __METHOD__); return $this->_components[$id] = Yii::createObject($this->_components[$id]); } } diff --git a/framework/db/Command.php b/framework/db/Command.php index ecd3674..a30aa14 100644 --- a/framework/db/Command.php +++ b/framework/db/Command.php @@ -134,7 +134,7 @@ class Command extends \yii\base\Component try { $this->pdoStatement = $this->db->pdo->prepare($sql); } catch (\Exception $e) { - Yii::error($e->getMessage() . "\nFailed to prepare SQL: $sql", __CLASS__); + Yii::error($e->getMessage() . "\nFailed to prepare SQL: $sql", __METHOD__); $errorInfo = $e instanceof \PDOException ? $e->errorInfo : null; throw new Exception($e->getMessage(), $errorInfo, (int)$e->getCode()); } @@ -266,15 +266,16 @@ class Command extends \yii\base\Component $paramLog = "\nParameters: " . var_export($this->_params, true); } - Yii::trace("Executing SQL: {$sql}{$paramLog}", __CLASS__); + Yii::trace("Executing SQL: {$sql}{$paramLog}", __METHOD__); if ($sql == '') { return 0; } try { + $token = "SQL: $sql"; if ($this->db->enableProfiling) { - Yii::beginProfile(__METHOD__ . "($sql)", __CLASS__); + Yii::beginProfile($token, __METHOD__); } $this->prepare(); @@ -282,16 +283,16 @@ class Command extends \yii\base\Component $n = $this->pdoStatement->rowCount(); if ($this->db->enableProfiling) { - Yii::endProfile(__METHOD__ . "($sql)", __CLASS__); + Yii::endProfile($token, __METHOD__); } return $n; } catch (\Exception $e) { if ($this->db->enableProfiling) { - Yii::endProfile(__METHOD__ . "($sql)", __CLASS__); + Yii::endProfile($token, __METHOD__); } $message = $e->getMessage(); - Yii::error("$message\nFailed to execute SQL: {$sql}{$paramLog}", __CLASS__); + Yii::error("$message\nFailed to execute SQL: {$sql}{$paramLog}", __METHOD__); $errorInfo = $e instanceof \PDOException ? $e->errorInfo : null; throw new Exception($message, $errorInfo, (int)$e->getCode()); @@ -383,7 +384,7 @@ class Command extends \yii\base\Component $paramLog = "\nParameters: " . var_export($this->_params, true); } - Yii::trace("Querying SQL: {$sql}{$paramLog}", __CLASS__); + Yii::trace("Querying SQL: {$sql}{$paramLog}", __METHOD__); /** @var $cache \yii\caching\Cache */ if ($db->enableQueryCache && $method !== '') { @@ -399,14 +400,15 @@ class Command extends \yii\base\Component $paramLog, )); if (($result = $cache->get($cacheKey)) !== false) { - Yii::trace('Query result served from cache', __CLASS__); + Yii::trace('Query result served from cache', __METHOD__); return $result; } } try { + $token = "SQL: $sql"; if ($db->enableProfiling) { - Yii::beginProfile(__METHOD__ . "($sql)", __CLASS__); + Yii::beginProfile($token, __METHOD__); } $this->prepare(); @@ -423,21 +425,21 @@ class Command extends \yii\base\Component } if ($db->enableProfiling) { - Yii::endProfile(__METHOD__ . "($sql)", __CLASS__); + Yii::endProfile($token, __METHOD__); } if (isset($cache, $cacheKey) && $cache instanceof Cache) { $cache->set($cacheKey, $result, $db->queryCacheDuration, $db->queryCacheDependency); - Yii::trace('Saved query result in cache', __CLASS__); + Yii::trace('Saved query result in cache', __METHOD__); } return $result; } catch (\Exception $e) { if ($db->enableProfiling) { - Yii::endProfile(__METHOD__ . "($sql)", __CLASS__); + Yii::endProfile($token, __METHOD__); } $message = $e->getMessage(); - Yii::error("$message\nCommand::$method() failed: {$sql}{$paramLog}", __CLASS__); + Yii::error("$message\nCommand::$method() failed: {$sql}{$paramLog}", __METHOD__); $errorInfo = $e instanceof \PDOException ? $e->errorInfo : null; throw new Exception($message, $errorInfo, (int)$e->getCode()); } diff --git a/framework/db/Connection.php b/framework/db/Connection.php index 59e8422..e84970b 100644 --- a/framework/db/Connection.php +++ b/framework/db/Connection.php @@ -324,12 +324,12 @@ class Connection extends Component throw new InvalidConfigException('Connection::dsn cannot be empty.'); } try { - \Yii::trace('Opening DB connection: ' . $this->dsn, __CLASS__); + \Yii::trace('Opening DB connection: ' . $this->dsn, __METHOD__); $this->pdo = $this->createPdoInstance(); $this->initConnection(); } catch (\PDOException $e) { - \Yii::error("Failed to open DB connection ({$this->dsn}): " . $e->getMessage(), __CLASS__); + \Yii::error("Failed to open DB connection ({$this->dsn}): " . $e->getMessage(), __METHOD__); $message = YII_DEBUG ? 'Failed to open DB connection: ' . $e->getMessage() : 'Failed to open DB connection.'; throw new Exception($message, $e->errorInfo, (int)$e->getCode()); } @@ -343,7 +343,7 @@ class Connection extends Component public function close() { if ($this->pdo !== null) { - \Yii::trace('Closing DB connection: ' . $this->dsn, __CLASS__); + \Yii::trace('Closing DB connection: ' . $this->dsn, __METHOD__); $this->pdo = null; $this->_schema = null; $this->_transaction = null; diff --git a/framework/db/Transaction.php b/framework/db/Transaction.php index 177d2cb..d66c38e 100644 --- a/framework/db/Transaction.php +++ b/framework/db/Transaction.php @@ -66,7 +66,7 @@ class Transaction extends \yii\base\Object if ($this->db === null) { throw new InvalidConfigException('Transaction::db must be set.'); } - \Yii::trace('Starting transaction', __CLASS__); + \Yii::trace('Starting transaction', __METHOD__); $this->db->open(); $this->db->pdo->beginTransaction(); $this->_active = true; @@ -80,7 +80,7 @@ class Transaction extends \yii\base\Object public function commit() { if ($this->_active && $this->db && $this->db->isActive) { - \Yii::trace('Committing transaction', __CLASS__); + \Yii::trace('Committing transaction', __METHOD__); $this->db->pdo->commit(); $this->_active = false; } else { @@ -95,7 +95,7 @@ class Transaction extends \yii\base\Object public function rollback() { if ($this->_active && $this->db && $this->db->isActive) { - \Yii::trace('Rolling back transaction', __CLASS__); + \Yii::trace('Rolling back transaction', __METHOD__); $this->db->pdo->rollBack(); $this->_active = false; } else { diff --git a/framework/i18n/PhpMessageSource.php b/framework/i18n/PhpMessageSource.php index 6b12353..1ada44a 100644 --- a/framework/i18n/PhpMessageSource.php +++ b/framework/i18n/PhpMessageSource.php @@ -72,7 +72,7 @@ class PhpMessageSource extends MessageSource } return $messages; } else { - Yii::error("The message file for category '$category' does not exist: $messageFile", __CLASS__); + Yii::error("The message file for category '$category' does not exist: $messageFile", __METHOD__); return array(); } } diff --git a/framework/web/Identity.php b/framework/web/Identity.php index 4668337..805d3d4 100644 --- a/framework/web/Identity.php +++ b/framework/web/Identity.php @@ -16,15 +16,14 @@ interface Identity { /** * Returns an ID that can uniquely identify a user identity. - * The returned ID can be a string, an integer, or any serializable data. - * @return mixed an ID that uniquely identifies a user identity. + * @return string|integer an ID that uniquely identifies a user identity. */ public function getId(); /** * Returns a key that can be used to check the validity of a given identity ID. * The space of such keys should be big and random enough to defeat potential identity attacks. * The returned key can be a string, an integer, or any serializable data. - * @return mixed a key that is used to check the validity of a given identity ID. + * @return string a key that is used to check the validity of a given identity ID. * @see validateAuthKey() */ public function getAuthKey(); @@ -37,7 +36,7 @@ interface Identity public function validateAuthKey($authKey); /** * Finds an identity by the given ID. - * @param mixed $id the ID to be looked for + * @param string|integer $id the ID to be looked for * @return Identity the identity object that matches the given ID. * Null should be returned if such an identity cannot be found. */ diff --git a/framework/web/Session.php b/framework/web/Session.php index c289db2..3e0f599 100644 --- a/framework/web/Session.php +++ b/framework/web/Session.php @@ -117,7 +117,7 @@ class Session extends Component implements \IteratorAggregate, \ArrayAccess, \Co $this->_opened = false; $error = error_get_last(); $message = isset($error['message']) ? $error['message'] : 'Failed to start session.'; - Yii::error($message, __CLASS__); + Yii::error($message, __METHOD__); } else { $this->_opened = true; $this->updateFlashCounters(); diff --git a/framework/web/User.php b/framework/web/User.php index 2326a10..74b5f18 100644 --- a/framework/web/User.php +++ b/framework/web/User.php @@ -90,6 +90,9 @@ class User extends Component { parent::init(); + if ($this->identityClass === null) { + throw new InvalidConfigException('User::identityClass must be set.'); + } if ($this->enableAutoLogin && !isset($this->identityCookie['name'])) { throw new InvalidConfigException('User::identityCookie must contain the "name" element.'); } @@ -179,7 +182,13 @@ class User extends Component /** @var $class Identity */ $class = $this->identityClass; $identity = $class::findIdentity($id); - if ($identity !== null && $identity->validateAuthKey($authKey) && $this->beforeLogin($identity, true)) { + if ($identity === null || !$identity->validateAuthKey($authKey)) { + if ($identity !== null) { + Yii::warning("Invalid auth key attempted for user '$id': $authKey", __METHOD__); + } + return; + } + if ($this->beforeLogin($identity, true)) { $this->switchIdentity($identity); if ($this->autoRenewCookie) { $this->saveIdentityCookie($identity, $duration); From 4d59587320160248838036163ef205fc5730efc4 Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Fri, 29 Mar 2013 01:10:03 +0100 Subject: [PATCH 002/104] fixed constructor overriding in test classes now use setUp() instead --- tests/unit/MysqlTestCase.php | 4 ++-- tests/unit/framework/caching/DbCacheTest.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/unit/MysqlTestCase.php b/tests/unit/MysqlTestCase.php index d62f95e..e1a1f7e 100644 --- a/tests/unit/MysqlTestCase.php +++ b/tests/unit/MysqlTestCase.php @@ -4,7 +4,7 @@ namespace yiiunit; class MysqlTestCase extends TestCase { - function __construct() + protected function setUp() { if (!extension_loaded('pdo') || !extension_loaded('pdo_mysql')) { $this->markTestSkipped('pdo and pdo_mysql extensions are required.'); @@ -15,7 +15,7 @@ class MysqlTestCase extends TestCase * @param bool $reset whether to clean up the test database * @return \yii\db\Connection */ - function getConnection($reset = true) + public function getConnection($reset = true) { $params = $this->getParam('mysql'); $db = new \yii\db\Connection; diff --git a/tests/unit/framework/caching/DbCacheTest.php b/tests/unit/framework/caching/DbCacheTest.php index 3977ee8..594e946 100644 --- a/tests/unit/framework/caching/DbCacheTest.php +++ b/tests/unit/framework/caching/DbCacheTest.php @@ -11,7 +11,7 @@ class DbCacheTest extends CacheTest private $_cacheInstance; private $_connection; - function __construct() + protected function setUp() { if (!extension_loaded('pdo') || !extension_loaded('pdo_mysql')) { $this->markTestSkipped('pdo and pdo_mysql extensions are required.'); From 6aa86712e2d5fa37bbd632dbab553bb9b4623a50 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Thu, 28 Mar 2013 21:51:31 -0400 Subject: [PATCH 003/104] User WIP --- framework/web/Application.php | 30 ++++- framework/web/Controller.php | 15 +++ framework/web/Identity.php | 3 +- framework/web/Response.php | 65 ++++++----- framework/web/User.php | 252 +++++++++++------------------------------- 5 files changed, 147 insertions(+), 218 deletions(-) diff --git a/framework/web/Application.php b/framework/web/Application.php index 2533f04..b839d92 100644 --- a/framework/web/Application.php +++ b/framework/web/Application.php @@ -7,7 +7,7 @@ namespace yii\web; -use yii\base\InvalidParamException; +use Yii; /** * Application is the base class for all application classes. @@ -28,7 +28,7 @@ class Application extends \yii\base\Application public function registerDefaultAliases() { parent::registerDefaultAliases(); - \Yii::$aliases['@webroot'] = dirname($_SERVER['SCRIPT_FILENAME']); + Yii::$aliases['@webroot'] = dirname($_SERVER['SCRIPT_FILENAME']); } /** @@ -41,6 +41,32 @@ class Application extends \yii\base\Application return $this->runAction($route, $params); } + private $_homeUrl; + + /** + * @return string the homepage URL + */ + public function getHomeUrl() + { + if ($this->_homeUrl === null) { + if ($this->getUrlManager()->showScriptName) { + return $this->getRequest()->getScriptUrl(); + } else { + return $this->getRequest()->getBaseUrl() . '/'; + } + } else { + return $this->_homeUrl; + } + } + + /** + * @param string $value the homepage URL + */ + public function setHomeUrl($value) + { + $this->_homeUrl = $value; + } + /** * Returns the request component. * @return Request the request component diff --git a/framework/web/Controller.php b/framework/web/Controller.php index 93b74aa..126fbbc 100644 --- a/framework/web/Controller.php +++ b/framework/web/Controller.php @@ -8,6 +8,7 @@ namespace yii\web; use Yii; +use yii\helpers\Html; /** * Controller is the base class of Web controllers. @@ -41,4 +42,18 @@ class Controller extends \yii\base\Controller return Yii::$app->getUrlManager()->createUrl($route, $params); } + /** + * Redirects the browser to the specified URL or route (controller/action). + * @param mixed $url the URL to be redirected to. If the parameter is an array, + * the first element must be a route to a controller action and the rest + * are GET parameters in name-value pairs. + * @param boolean $terminate whether to terminate the current application after calling this method. Defaults to true. + * @param integer $statusCode the HTTP status code. Defaults to 302. See {@link http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html} + * for details about HTTP status code. + */ + public function redirect($url, $terminate = true, $statusCode = 302) + { + $url = Html::url($url); + Yii::$app->getResponse()->redirect($url, $terminate, $statusCode); + } } \ No newline at end of file diff --git a/framework/web/Identity.php b/framework/web/Identity.php index 805d3d4..5f07d58 100644 --- a/framework/web/Identity.php +++ b/framework/web/Identity.php @@ -38,7 +38,8 @@ interface Identity * Finds an identity by the given ID. * @param string|integer $id the ID to be looked for * @return Identity the identity object that matches the given ID. - * Null should be returned if such an identity cannot be found. + * Null should be returned if such an identity cannot be found + * or the identity is not in an active state (disabled, deleted, etc.) */ public static function findIdentity($id); } \ No newline at end of file diff --git a/framework/web/Response.php b/framework/web/Response.php index d23c5b9..8133132 100644 --- a/framework/web/Response.php +++ b/framework/web/Response.php @@ -107,37 +107,42 @@ class Response extends \yii\base\Response *
  • addHeaders: an array of additional http headers in header-value pairs (available since version 1.1.10)
  • * */ - public function xSendFile($filePath, $options=array()) + public function xSendFile($filePath, $options = array()) { - if(!isset($options['forceDownload']) || $options['forceDownload']) - $disposition='attachment'; - else - $disposition='inline'; + if (!isset($options['forceDownload']) || $options['forceDownload']) { + $disposition = 'attachment'; + } else { + $disposition = 'inline'; + } - if(!isset($options['saveName'])) - $options['saveName']=basename($filePath); + if (!isset($options['saveName'])) { + $options['saveName'] = basename($filePath); + } - if(!isset($options['mimeType'])) - { - if(($options['mimeType']=CFileHelper::getMimeTypeByExtension($filePath))===null) - $options['mimeType']='text/plain'; + if (!isset($options['mimeType'])) { + if (($options['mimeType'] = CFileHelper::getMimeTypeByExtension($filePath)) === null) { + $options['mimeType'] = 'text/plain'; + } } - if(!isset($options['xHeader'])) - $options['xHeader']='X-Sendfile'; + if (!isset($options['xHeader'])) { + $options['xHeader'] = 'X-Sendfile'; + } - if($options['mimeType'] !== null) - header('Content-type: '.$options['mimeType']); - header('Content-Disposition: '.$disposition.'; filename="'.$options['saveName'].'"'); - if(isset($options['addHeaders'])) - { - foreach($options['addHeaders'] as $header=>$value) - header($header.': '.$value); + if ($options['mimeType'] !== null) { + header('Content-type: ' . $options['mimeType']); + } + header('Content-Disposition: ' . $disposition . '; filename="' . $options['saveName'] . '"'); + if (isset($options['addHeaders'])) { + foreach ($options['addHeaders'] as $header => $value) { + header($header . ': ' . $value); + } } - header(trim($options['xHeader']).': '.$filePath); + header(trim($options['xHeader']) . ': ' . $filePath); - if(!isset($options['terminate']) || $options['terminate']) - Yii::app()->end(); + if (!isset($options['terminate']) || $options['terminate']) { + Yii::$app->end(); + } } /** @@ -148,13 +153,15 @@ class Response extends \yii\base\Response * @param integer $statusCode the HTTP status code. Defaults to 302. See {@link http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html} * for details about HTTP status code. */ - public function redirect($url,$terminate=true,$statusCode=302) + public function redirect($url, $terminate = true, $statusCode = 302) { - if(strpos($url,'/')===0 && strpos($url,'//')!==0) - $url=$this->getHostInfo().$url; - header('Location: '.$url, true, $statusCode); - if($terminate) - Yii::app()->end(); + if (strpos($url, '/') === 0 && strpos($url, '//') !== 0) { + $url = Yii::$app->getRequest()->getHostInfo() . $url; + } + header('Location: ' . $url, true, $statusCode); + if ($terminate) { + Yii::$app->end(); + } } diff --git a/framework/web/User.php b/framework/web/User.php index 74b5f18..aa9e421 100644 --- a/framework/web/User.php +++ b/framework/web/User.php @@ -9,7 +9,9 @@ namespace yii\web; use Yii; use yii\base\Component; +use yii\base\HttpException; use yii\base\InvalidConfigException; +use yii\helpers\Html; /** * @author Qiang Xue @@ -17,15 +19,21 @@ use yii\base\InvalidConfigException; */ class User extends Component { - const ID_VAR = '__id'; - const AUTH_EXPIRE_VAR = '__expire'; - const EVENT_BEFORE_LOGIN = 'beforeLogin'; const EVENT_AFTER_LOGIN = 'afterLogin'; const EVENT_BEFORE_LOGOUT = 'beforeLogout'; const EVENT_AFTER_LOGOUT = 'afterLogout'; /** + * @var Identity the identity object associated with the currently logged user. + * This property is set automatically be the User component. Do not modify it directly + * unless you understand the consequence. You should normally use [[login()]], [[logout()]], + * or [[switchIdentity()]] to update the identity associated with the current user. + * + * If this property is null, it means the current user is a guest (not authenticated). + */ + public $identity; + /** * @var string the class name of the [[identity]] object. */ public $identityClass; @@ -64,24 +72,12 @@ class User extends Component * is initially logged in. When this is true, the identity cookie will expire after the specified duration * since the user visits the site the last time. * @see enableAutoLogin - * @since 1.1.0 - */ - public $autoRenewCookie = false; - /** - * @var string value that will be echoed in case that user session has expired during an ajax call. - * When a request is made and user session has expired, {@link loginRequired} redirects to {@link loginUrl} for login. - * If that happens during an ajax call, the complete HTML login page is returned as the result of that ajax call. That could be - * a problem if the ajax call expects the result to be a json array or a predefined string, as the login page is ignored in that case. - * To solve this, set this property to the desired return value. - * - * If this property is set, this value will be returned as the result of the ajax call in case that the user session has expired. - * @since 1.1.9 - * @see loginRequired */ - public $loginRequiredAjaxResponse; - + public $autoRenewCookie = true; - public $stateVar = '__states'; + public $idSessionVar = '__id'; + public $authTimeoutSessionVar = '__expire'; + public $returnUrlSessionVar = '__returnUrl'; /** * Initializes the application component. @@ -99,6 +95,8 @@ class User extends Component Yii::$app->getSession()->open(); + $this->loadIdentity(); + $this->renewAuthStatus(); if ($this->enableAutoLogin) { @@ -110,29 +108,16 @@ class User extends Component } } - /** - * @var Identity the identity object associated with the currently logged user. - */ - private $_identity = false; - - public function getIdentity() + public function loadIdentity() { - if ($this->_identity === false) { - $id = $this->getId(); - if ($id === null) { - $this->_identity = null; - } else { - /** @var $class Identity */ - $class = $this->identityClass; - $this->_identity = $class::findIdentity($this->getId()); - } + $id = $this->getId(); + if ($id === null) { + $this->identity = null; + } else { + /** @var $class Identity */ + $class = $this->identityClass; + $this->identity = $class::findIdentity($this->getId()); } - return $this->_identity; - } - - public function setIdentity($identity) - { - $this->switchIdentity($identity); } /** @@ -157,7 +142,7 @@ class User extends Component if ($this->beforeLogin($identity, false)) { $this->switchIdentity($identity); if ($duration > 0 && $this->enableAutoLogin) { - $this->saveIdentityCookie($identity, $duration); + $this->sendIdentityCookie($identity, $duration); } $this->afterLogin($identity, false); } @@ -169,7 +154,7 @@ class User extends Component * This method is used when automatic login ({@link enableAutoLogin}) is enabled. * The user identity information is recovered from cookie. * Sufficient security measures are used to prevent cookie data from being tampered. - * @see saveIdentityCookie + * @see sendIdentityCookie */ protected function loginByCookie() { @@ -182,18 +167,16 @@ class User extends Component /** @var $class Identity */ $class = $this->identityClass; $identity = $class::findIdentity($id); - if ($identity === null || !$identity->validateAuthKey($authKey)) { - if ($identity !== null) { - Yii::warning("Invalid auth key attempted for user '$id': $authKey", __METHOD__); + if ($identity !== null && $identity->validateAuthKey($authKey)) { + if ($this->beforeLogin($identity, true)) { + $this->switchIdentity($identity); + if ($this->autoRenewCookie) { + $this->sendIdentityCookie($identity, $duration); + } + $this->afterLogin($identity, true); } - return; - } - if ($this->beforeLogin($identity, true)) { - $this->switchIdentity($identity); - if ($this->autoRenewCookie) { - $this->saveIdentityCookie($identity, $duration); - } - $this->afterLogin($identity, true); + } elseif ($identity !== null) { + Yii::warning("Invalid auth key attempted for user '$id': $authKey", __METHOD__); } } } @@ -208,7 +191,7 @@ class User extends Component */ public function logout($destroySession = true) { - $identity = $this->getIdentity(); + $identity = $this->identity; if ($identity !== null && $this->beforeLogout($identity)) { $this->switchIdentity(null); if ($this->enableAutoLogin) { @@ -227,7 +210,7 @@ class User extends Component */ public function getIsGuest() { - return $this->getIdentity() === null; + return $this->identity === null; } /** @@ -236,7 +219,7 @@ class User extends Component */ public function getId() { - return $this->getState(static::ID_VAR); + return Yii::$app->getSession()->get($this->idSessionVar); } /** @@ -244,7 +227,7 @@ class User extends Component */ public function setId($value) { - $this->setState(static::ID_VAR, $value); + Yii::$app->getSession()->set($this->idSessionVar, $value); } /** @@ -258,12 +241,12 @@ class User extends Component */ public function getReturnUrl($defaultUrl = null) { - if ($defaultUrl === null) { - $defaultReturnUrl = Yii::app()->getUrlManager()->showScriptName ? Yii::app()->getRequest()->getScriptUrl() : Yii::app()->getRequest()->getBaseUrl() . '/'; + $url = Yii::$app->getSession()->get($this->returnUrlSessionVar, $defaultUrl); + if ($url === null) { + return Yii::$app->getHomeUrl(); } else { - $defaultReturnUrl = CHtml::normalizeUrl($defaultUrl); + return Html::url($url); } - return $this->getState('__returnUrl', $defaultReturnUrl); } /** @@ -271,7 +254,7 @@ class User extends Component */ public function setReturnUrl($value) { - $this->setState('__returnUrl', $value); + Yii::$app->getSession()->set($this->returnUrlSessionVar, $value); } /** @@ -285,24 +268,22 @@ class User extends Component */ public function loginRequired() { - $app = Yii::app(); - $request = $app->getRequest(); - - if (!$request->getIsAjaxRequest()) { - $this->setReturnUrl($request->getUrl()); - } elseif (isset($this->loginRequiredAjaxResponse)) { - echo $this->loginRequiredAjaxResponse; - Yii::app()->end(); - } - if (($url = $this->loginUrl) !== null) { - if (is_array($url)) { - $route = isset($url[0]) ? $url[0] : $app->defaultController; - $url = $app->createUrl($route, array_splice($url, 1)); + $url = Html::url($url); + $request = Yii::$app->getRequest(); + if (strpos($url, '/') === 0 && strpos($url, '//') !== 0) { + $url = $request->getHostInfo() . $url; + } + if ($request->getIsAjaxRequest()) { + echo json_encode(array( + 'redirect' => $url, + )); + Yii::$app->end(); + } else { + Yii::$app->getResponse()->redirect($url); } - $request->redirect($url); } else { - throw new CHttpException(403, Yii::t('yii', 'Login Required')); + throw new HttpException(403, Yii::t('yii|Login Required')); } } @@ -399,7 +380,7 @@ class User extends Component * @param integer $duration number of seconds that the user can remain in logged-in status. Defaults to 0, meaning login till the user closes the browser. * @see loginByCookie */ - protected function saveIdentityCookie($identity, $duration) + protected function sendIdentityCookie($identity, $duration) { $cookie = new Cookie($this->identityCookie); $cookie->value = json_encode(array( @@ -423,14 +404,16 @@ class User extends Component protected function switchIdentity($identity) { Yii::$app->getSession()->regenerateID(true); - $this->setIdentity($identity); + $this->identity = $identity; if ($identity instanceof Identity) { $this->setId($identity->getId()); if ($this->authTimeout !== null) { - $this->setState(self::AUTH_EXPIRE_VAR, time() + $this->authTimeout); + Yii::$app->getSession()->set($this->authTimeoutSessionVar, time() + $this->authTimeout); } } else { - $this->removeAllStates(); + $session = Yii::$app->getSession(); + $session->remove($this->idSessionVar); + $session->remove($this->authTimeoutSessionVar); } } @@ -442,115 +425,12 @@ class User extends Component protected function renewAuthStatus() { if ($this->authTimeout !== null && !$this->getIsGuest()) { - $expire = $this->getState(self::AUTH_EXPIRE_VAR); + $expire = Yii::$app->getSession()->get($this->authTimeoutSessionVar); if ($expire !== null && $expire < time()) { $this->logout(false); } else { - $this->setState(self::AUTH_EXPIRE_VAR, time() + $this->authTimeout); + Yii::$app->getSession()->set($this->authTimeoutSessionVar, time() + $this->authTimeout); } } } - - /** - * Returns a user state. - * A user state is a session data item associated with the current user. - * If the user logs out, all his/her user states will be removed. - * @param string $key the key identifying the state - * @param mixed $defaultValue value to be returned if the state does not exist. - * @return mixed the state - */ - public function getState($key, $defaultValue = null) - { - $manifest = isset($_SESSION[$this->stateVar]) ? $_SESSION[$this->stateVar] : null; - if (is_array($manifest) && isset($manifest[$key], $_SESSION[$key])) { - return $_SESSION[$key]; - } else { - return $defaultValue; - } - } - - /** - * Returns all user states. - * @return array states (key => state). - */ - public function getAllStates() - { - $manifest = isset($_SESSION[$this->stateVar]) ? $_SESSION[$this->stateVar] : null; - $states = array(); - if (is_array($manifest)) { - foreach (array_keys($manifest) as $key) { - if (isset($_SESSION[$key])) { - $states[$key] = $_SESSION[$key]; - } - } - } - return $states; - } - - /** - * Stores a user state. - * A user state is a session data item associated with the current user. - * If the user logs out, all his/her user states will be removed. - * @param string $key the key identifying the state. Note that states - * and normal session variables share the same name space. If you have a normal - * session variable using the same name, its value will be overwritten by this method. - * @param mixed $value state - */ - public function setState($key, $value) - { - $manifest = isset($_SESSION[$this->stateVar]) ? $_SESSION[$this->stateVar] : array(); - $manifest[$value] = true; - $_SESSION[$key] = $value; - $_SESSION[$this->stateVar] = $manifest; - } - - /** - * Removes a user state. - * If the user logs out, all his/her user states will be removed automatically. - * @param string $key the key identifying the state. Note that states - * and normal session variables share the same name space. If you have a normal - * session variable using the same name, it will be removed by this method. - * @return mixed the removed state. Null if the state does not exist. - */ - public function removeState($key) - { - $manifest = isset($_SESSION[$this->stateVar]) ? $_SESSION[$this->stateVar] : null; - if (is_array($manifest) && isset($manifest[$key], $_SESSION[$key])) { - $value = $_SESSION[$key]; - } else { - $value = null; - } - unset($_SESSION[$this->stateVar][$key], $_SESSION[$key]); - return $value; - } - - /** - * Removes all states. - * If the user logs out, all his/her user states will be removed automatically - * without the need to call this method manually. - * - * Note that states and normal session variables share the same name space. - * If you have a normal session variable using the same name, it will be removed - * by this method. - */ - public function removeAllStates() - { - $manifest = isset($_SESSION[$this->stateVar]) ? $_SESSION[$this->stateVar] : null; - if (is_array($manifest)) { - foreach (array_keys($manifest) as $key) { - unset($_SESSION[$key]); - } - } - unset($_SESSION[$this->stateVar]); - } - - /** - * Returns a value indicating whether there is a state associated with the specified key. - * @param string $key key identifying the state - * @return boolean whether the specified state exists - */ - public function hasState($key) - { - return $this->getState($key) !== null; - } } From c0d0d1dc357b7b79307a751972f9e9be10606a0c Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Thu, 28 Mar 2013 23:53:36 -0400 Subject: [PATCH 004/104] Finished User. --- framework/web/Controller.php | 15 ---- framework/web/Identity.php | 20 +++-- framework/web/Response.php | 38 +++++++- framework/web/User.php | 201 +++++++++++++++++++++---------------------- framework/web/UserEvent.php | 2 +- 5 files changed, 146 insertions(+), 130 deletions(-) diff --git a/framework/web/Controller.php b/framework/web/Controller.php index 126fbbc..8049299 100644 --- a/framework/web/Controller.php +++ b/framework/web/Controller.php @@ -41,19 +41,4 @@ class Controller extends \yii\base\Controller } return Yii::$app->getUrlManager()->createUrl($route, $params); } - - /** - * Redirects the browser to the specified URL or route (controller/action). - * @param mixed $url the URL to be redirected to. If the parameter is an array, - * the first element must be a route to a controller action and the rest - * are GET parameters in name-value pairs. - * @param boolean $terminate whether to terminate the current application after calling this method. Defaults to true. - * @param integer $statusCode the HTTP status code. Defaults to 302. See {@link http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html} - * for details about HTTP status code. - */ - public function redirect($url, $terminate = true, $statusCode = 302) - { - $url = Html::url($url); - Yii::$app->getResponse()->redirect($url, $terminate, $statusCode); - } } \ No newline at end of file diff --git a/framework/web/Identity.php b/framework/web/Identity.php index 5f07d58..89d4282 100644 --- a/framework/web/Identity.php +++ b/framework/web/Identity.php @@ -15,6 +15,14 @@ namespace yii\web; interface Identity { /** + * Finds an identity by the given ID. + * @param string|integer $id the ID to be looked for + * @return Identity the identity object that matches the given ID. + * Null should be returned if such an identity cannot be found + * or the identity is not in an active state (disabled, deleted, etc.) + */ + public static function findIdentity($id); + /** * Returns an ID that can uniquely identify a user identity. * @return string|integer an ID that uniquely identifies a user identity. */ @@ -23,23 +31,19 @@ interface Identity * Returns a key that can be used to check the validity of a given identity ID. * The space of such keys should be big and random enough to defeat potential identity attacks. * The returned key can be a string, an integer, or any serializable data. + * + * This is required if [[User::enableAutoLogin]] is enabled. * @return string a key that is used to check the validity of a given identity ID. * @see validateAuthKey() */ public function getAuthKey(); /** * Validates the given auth key. + * + * This is required if [[User::enableAutoLogin]] is enabled. * @param string $authKey the given auth key * @return boolean whether the given auth key is valid. * @see getAuthKey() */ public function validateAuthKey($authKey); - /** - * Finds an identity by the given ID. - * @param string|integer $id the ID to be looked for - * @return Identity the identity object that matches the given ID. - * Null should be returned if such an identity cannot be found - * or the identity is not in an active state (disabled, deleted, etc.) - */ - public static function findIdentity($id); } \ No newline at end of file diff --git a/framework/web/Response.php b/framework/web/Response.php index 8133132..da2482f 100644 --- a/framework/web/Response.php +++ b/framework/web/Response.php @@ -9,6 +9,7 @@ namespace yii\web; use Yii; use yii\helpers\FileHelper; +use yii\helpers\Html; /** * @author Qiang Xue @@ -17,6 +18,14 @@ use yii\helpers\FileHelper; class Response extends \yii\base\Response { /** + * @var integer the HTTP status code that should be used when redirecting in AJAX mode. + * This is used by [[redirect()]]. A 2xx code should normally be used for this purpose + * so that the AJAX handler will treat the response as a success. + * @see redirect + */ + public $ajaxRedirectCode = 278; + + /** * Sends a file to user. * @param string $fileName file name * @param string $content content to be set. @@ -147,24 +156,45 @@ class Response extends \yii\base\Response /** * Redirects the browser to the specified URL. - * @param string $url URL to be redirected to. Note that when URL is not - * absolute (not starting with "/") it will be relative to current request URL. + * This method will send out a "Location" header to achieve the redirection. + * In AJAX mode, this normally will not work as expected unless there are some + * client-side JavaScript code handling the redirection. To help achieve this goal, + * this method will use [[ajaxRedirectCode]] as the HTTP status code when performing + * redirection in AJAX mode. The following JavaScript code may be used on the client + * side to handle the redirection response: + * + * ~~~ + * $(document).ajaxSuccess(function(event, xhr, settings) { + * if (xhr.status == 278) { + * window.location = xhr.getResponseHeader('Location'); + * } + * }); + * ~~~ + * + * @param array|string $url the URL to be redirected to. [[\yii\helpers\Html::url()]] + * will be used to normalize the URL. If the resulting URL is still a relative URL + * (one without host info), the current request host info will be used. * @param boolean $terminate whether to terminate the current application - * @param integer $statusCode the HTTP status code. Defaults to 302. See {@link http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html} + * @param integer $statusCode the HTTP status code. Defaults to 302. + * See [[http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html]] * for details about HTTP status code. + * Note that if the request is an AJAX request, [[ajaxRedirectCode]] will be used instead. */ public function redirect($url, $terminate = true, $statusCode = 302) { + $url = Html::url($url); if (strpos($url, '/') === 0 && strpos($url, '//') !== 0) { $url = Yii::$app->getRequest()->getHostInfo() . $url; } + if (Yii::$app->getRequest()->getIsAjaxRequest()) { + $statusCode = $this->ajaxRedirectCode; + } header('Location: ' . $url, true, $statusCode); if ($terminate) { Yii::$app->end(); } } - /** * Returns the cookie collection. * Through the returned cookie collection, you add or remove cookies as follows, diff --git a/framework/web/User.php b/framework/web/User.php index aa9e421..ebfcd03 100644 --- a/framework/web/User.php +++ b/framework/web/User.php @@ -11,7 +11,6 @@ use Yii; use yii\base\Component; use yii\base\HttpException; use yii\base\InvalidConfigException; -use yii\helpers\Html; /** * @author Qiang Xue @@ -67,17 +66,27 @@ class User extends Component public $authTimeout; /** * @var boolean whether to automatically renew the identity cookie each time a page is requested. - * Defaults to false. This property is effective only when {@link enableAutoLogin} is true. + * This property is effective only when [[enableAutoLogin]] is true. * When this is false, the identity cookie will expire after the specified duration since the user * is initially logged in. When this is true, the identity cookie will expire after the specified duration * since the user visits the site the last time. * @see enableAutoLogin */ public $autoRenewCookie = true; + /** + * @var string the session variable name used to store the value of [[id]]. + */ + public $idVar = '__id'; + /** + * @var string the session variable name used to store the value of expiration timestamp of the authenticated state. + * This is used when [[authTimeout]] is set. + */ + public $authTimeoutVar = '__expire'; + /** + * @var string the session variable name used to store the value of [[returnUrl]]. + */ + public $returnUrlVar = '__returnUrl'; - public $idSessionVar = '__id'; - public $authTimeoutSessionVar = '__expire'; - public $returnUrlSessionVar = '__returnUrl'; /** * Initializes the application component. @@ -108,7 +117,10 @@ class User extends Component } } - public function loadIdentity() + /** + * Loads the [[identity]] object according to [[id]]. + */ + protected function loadIdentity() { $id = $this->getId(); if ($id === null) { @@ -116,25 +128,22 @@ class User extends Component } else { /** @var $class Identity */ $class = $this->identityClass; - $this->identity = $class::findIdentity($this->getId()); + $this->identity = $class::findIdentity($id); } } /** * Logs in a user. * - * The user identity information will be saved in storage that is - * persistent during the user session. By default, the storage is simply - * the session storage. If the duration parameter is greater than 0, - * a cookie will be sent to prepare for cookie-based login in future. - * - * Note, you have to set {@link enableAutoLogin} to true - * if you want to allow user to be authenticated based on the cookie information. + * This method stores the necessary session information to keep track + * of the user identity information. If `$duration` is greater than 0 + * and [[enableAutoLogin]] is true, it will also send out an identity + * cookie to support cookie-based login. * * @param Identity $identity the user identity (which should already be authenticated) - * @param integer $duration number of seconds that the user can remain in logged-in status. Defaults to 0, meaning login till the user closes the browser. - * If greater than 0, cookie-based login will be used. In this case, {@link enableAutoLogin} - * must be set true, otherwise an exception will be thrown. + * @param integer $duration number of seconds that the user can remain in logged-in status. + * Defaults to 0, meaning login till the user closes the browser or the session is manually destroyed. + * If greater than 0 and [[enableAutoLogin]] is true, cookie-based login will be supported. * @return boolean whether the user is logged in */ public function login($identity, $duration = 0) @@ -150,11 +159,10 @@ class User extends Component } /** - * Populates the current user object with the information obtained from cookie. - * This method is used when automatic login ({@link enableAutoLogin}) is enabled. - * The user identity information is recovered from cookie. - * Sufficient security measures are used to prevent cookie data from being tampered. - * @see sendIdentityCookie + * Logs in a user by cookie. + * + * This method attempts to log in a user using the ID and authKey information + * provided by the given cookie. */ protected function loginByCookie() { @@ -185,9 +193,8 @@ class User extends Component /** * Logs out the current user. * This will remove authentication-related session data. - * If the parameter is true, the whole session will be destroyed as well. - * @param boolean $destroySession whether to destroy the whole session. Defaults to true. If false, - * then {@link clearStates} will be called, which removes only the data stored via {@link setState}. + * If `$destroySession` is true, all session data will be removed. + * @param boolean $destroySession whether to destroy the whole session. Defaults to true. */ public function logout($destroySession = true) { @@ -215,73 +222,63 @@ class User extends Component /** * Returns a value that uniquely represents the user. - * @return mixed the unique identifier for the user. If null, it means the user is a guest. + * @return string|integer the unique identifier for the user. If null, it means the user is a guest. */ public function getId() { - return Yii::$app->getSession()->get($this->idSessionVar); + return Yii::$app->getSession()->get($this->idVar); } /** - * @param mixed $value the unique identifier for the user. If null, it means the user is a guest. + * @param string|integer $value the unique identifier for the user. If null, it means the user is a guest. */ public function setId($value) { - Yii::$app->getSession()->set($this->idSessionVar, $value); + Yii::$app->getSession()->set($this->idVar, $value); } /** * Returns the URL that the user should be redirected to after successful login. * This property is usually used by the login action. If the login is successful, * the action should read this property and use it to redirect the user browser. - * @param string $defaultUrl the default return URL in case it was not set previously. If this is null, - * the application entry URL will be considered as the default return URL. + * @param string|array $defaultUrl the default return URL in case it was not set previously. + * If this is null, it means [[Application::homeUrl]] will be redirected to. + * Please refer to [[\yii\helpers\Html::url()]] on acceptable URL formats. * @return string the URL that the user should be redirected to after login. * @see loginRequired */ public function getReturnUrl($defaultUrl = null) { - $url = Yii::$app->getSession()->get($this->returnUrlSessionVar, $defaultUrl); - if ($url === null) { - return Yii::$app->getHomeUrl(); - } else { - return Html::url($url); - } + $url = Yii::$app->getSession()->get($this->returnUrlVar, $defaultUrl); + return $url === null ? Yii::$app->getHomeUrl() : $url; } /** - * @param string $value the URL that the user should be redirected to after login. + * @param string|array $url the URL that the user should be redirected to after login. + * Please refer to [[\yii\helpers\Html::url()]] on acceptable URL formats. */ - public function setReturnUrl($value) + public function setReturnUrl($url) { - Yii::$app->getSession()->set($this->returnUrlSessionVar, $value); + Yii::$app->getSession()->set($this->returnUrlVar, $url); } /** * Redirects the user browser to the login page. * Before the redirection, the current URL (if it's not an AJAX url) will be - * kept in {@link returnUrl} so that the user browser may be redirected back - * to the current page after successful login. Make sure you set {@link loginUrl} + * kept as [[returnUrl]] so that the user browser may be redirected back + * to the current page after successful login. Make sure you set [[loginUrl]] * so that the user browser can be redirected to the specified login URL after * calling this method. * After calling this method, the current request processing will be terminated. */ public function loginRequired() { - if (($url = $this->loginUrl) !== null) { - $url = Html::url($url); - $request = Yii::$app->getRequest(); - if (strpos($url, '/') === 0 && strpos($url, '//') !== 0) { - $url = $request->getHostInfo() . $url; - } - if ($request->getIsAjaxRequest()) { - echo json_encode(array( - 'redirect' => $url, - )); - Yii::$app->end(); - } else { - Yii::$app->getResponse()->redirect($url); - } + $request = Yii::$app->getRequest(); + if (!$request->getIsAjaxRequest()) { + $this->setReturnUrl($request->getUrl()); + } + if ($this->loginUrl !== null) { + Yii::$app->getResponse()->redirect($this->loginUrl); } else { throw new HttpException(403, Yii::t('yii|Login Required')); } @@ -289,21 +286,18 @@ class User extends Component /** * This method is called before logging in a user. - * You may override this method to provide additional security check. - * For example, when the login is cookie-based, you may want to verify - * that the user ID together with a random token in the states can be found - * in the database. This will prevent hackers from faking arbitrary - * identity cookies even if they crack down the server private key. - * @param mixed $id the user ID. This is the same as returned by {@link getId()}. - * @param array $states a set of name-value pairs that are provided by the user identity. - * @param boolean $fromCookie whether the login is based on cookie - * @return boolean whether the user should be logged in + * The default implementation will trigger the [[EVENT_BEFORE_LOGIN]] event. + * If you override this method, make sure you call the parent implementation + * so that the event is triggered. + * @param Identity $identity the user identity information + * @param boolean $cookieBased whether the login is cookie-based + * @return boolean whether the user should continue to be logged in */ - protected function beforeLogin($identity, $fromCookie) + protected function beforeLogin($identity, $cookieBased) { $event = new UserEvent(array( 'identity' => $identity, - 'fromCookie' => $fromCookie, + 'cookieBased' => $cookieBased, )); $this->trigger(self::EVENT_BEFORE_LOGIN, $event); return $event->isValid; @@ -311,24 +305,27 @@ class User extends Component /** * This method is called after the user is successfully logged in. - * You may override this method to do some postprocessing (e.g. log the user - * login IP and time; load the user permission information). - * @param boolean $fromCookie whether the login is based on cookie. + * The default implementation will trigger the [[EVENT_AFTER_LOGIN]] event. + * If you override this method, make sure you call the parent implementation + * so that the event is triggered. + * @param Identity $identity the user identity information + * @param boolean $cookieBased whether the login is cookie-based */ - protected function afterLogin($identity, $fromCookie) + protected function afterLogin($identity, $cookieBased) { $this->trigger(self::EVENT_AFTER_LOGIN, new UserEvent(array( 'identity' => $identity, - 'fromCookie' => $fromCookie, + 'cookieBased' => $cookieBased, ))); } /** - * This method is invoked when calling {@link logout} to log out a user. - * If this method return false, the logout action will be cancelled. - * You may override this method to provide additional check before - * logging out a user. - * @return boolean whether to log out the user + * This method is invoked when calling [[logout()]] to log out a user. + * The default implementation will trigger the [[EVENT_BEFORE_LOGOUT]] event. + * If you override this method, make sure you call the parent implementation + * so that the event is triggered. + * @param Identity $identity the user identity information + * @return boolean whether the user should continue to be logged out */ protected function beforeLogout($identity) { @@ -340,8 +337,11 @@ class User extends Component } /** - * This method is invoked right after a user is logged out. - * You may override this method to do some extra cleanup work for the user. + * This method is invoked right after a user is logged out via [[logout()]]. + * The default implementation will trigger the [[EVENT_AFTER_LOGOUT]] event. + * If you override this method, make sure you call the parent implementation + * so that the event is triggered. + * @param Identity $identity the user identity information */ protected function afterLogout($identity) { @@ -350,7 +350,6 @@ class User extends Component ))); } - /** * Renews the identity cookie. * This method will set the expiration time of the identity cookie to be the current time @@ -372,12 +371,12 @@ class User extends Component } /** - * Saves necessary user data into a cookie. - * This method is used when automatic login ({@link enableAutoLogin}) is enabled. - * This method saves user ID, username, other identity states and a validation key to cookie. - * These information are used to do authentication next time when user visits the application. + * Sends an identity cookie. + * This method is used when [[enableAutoLogin]] is true. + * It saves [[id]], [[Identity::getAuthKey()|auth key]], and the duration of cookie-based login + * information in the cookie. * @param Identity $identity - * @param integer $duration number of seconds that the user can remain in logged-in status. Defaults to 0, meaning login till the user closes the browser. + * @param integer $duration number of seconds that the user can remain in logged-in status. * @see loginByCookie */ protected function sendIdentityCookie($identity, $duration) @@ -394,42 +393,40 @@ class User extends Component /** * Changes the current user with the specified identity information. - * This method is called by {@link login} and {@link restoreFromCookie} - * when the current user needs to be populated with the corresponding - * identity information. Derived classes may override this method - * by retrieving additional user-related information. Make sure the - * parent implementation is called first. - * @param Identity $identity a unique identifier for the user + * This method is called by [[login()]] and [[loginByCookie()]] + * when the current user needs to be associated with the corresponding + * identity information. + * @param Identity $identity the identity information to be associated with the current user. */ protected function switchIdentity($identity) { Yii::$app->getSession()->regenerateID(true); $this->identity = $identity; + $session = Yii::$app->getSession(); + $session->remove($this->idVar); + $session->remove($this->authTimeoutVar); if ($identity instanceof Identity) { $this->setId($identity->getId()); if ($this->authTimeout !== null) { - Yii::$app->getSession()->set($this->authTimeoutSessionVar, time() + $this->authTimeout); + Yii::$app->getSession()->set($this->authTimeoutVar, time() + $this->authTimeout); } - } else { - $session = Yii::$app->getSession(); - $session->remove($this->idSessionVar); - $session->remove($this->authTimeoutSessionVar); } } /** - * Updates the authentication status according to {@link authTimeout}. - * If the user has been inactive for {@link authTimeout} seconds, - * he will be automatically logged out. + * Updates the authentication status according to [[authTimeout]]. + * This method is called during [[init()]]. + * It will update the user's authentication status if it has not outdated yet. + * Otherwise, it will logout the user. */ protected function renewAuthStatus() { - if ($this->authTimeout !== null && !$this->getIsGuest()) { - $expire = Yii::$app->getSession()->get($this->authTimeoutSessionVar); + if ($this->authTimeout !== null && $this->identity !== null) { + $expire = Yii::$app->getSession()->get($this->authTimeoutVar); if ($expire !== null && $expire < time()) { $this->logout(false); } else { - Yii::$app->getSession()->set($this->authTimeoutSessionVar, time() + $this->authTimeout); + Yii::$app->getSession()->set($this->authTimeoutVar, time() + $this->authTimeout); } } } diff --git a/framework/web/UserEvent.php b/framework/web/UserEvent.php index 3a8723a..6955ae5 100644 --- a/framework/web/UserEvent.php +++ b/framework/web/UserEvent.php @@ -24,7 +24,7 @@ class UserEvent extends Event * @var boolean whether the login is cookie-based. This property is only meaningful * for [[User::EVENT_BEFORE_LOGIN]] and [[User::EVENT_AFTER_LOGIN]] events. */ - public $fromCookie; + public $cookieBased; /** * @var boolean whether the login or logout should proceed. * Event handlers may modify this property to determine whether the login or logout should proceed. From 4d93e468d6e6d6f51b301a61fb722dd4e55510d7 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Fri, 29 Mar 2013 08:17:23 -0400 Subject: [PATCH 005/104] Fixed UserEvent bug. Updated docs. --- framework/web/User.php | 8 ++++++++ framework/web/UserEvent.php | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/framework/web/User.php b/framework/web/User.php index ebfcd03..5b5f977 100644 --- a/framework/web/User.php +++ b/framework/web/User.php @@ -13,6 +13,14 @@ use yii\base\HttpException; use yii\base\InvalidConfigException; /** + * User is an application component that manages the user authentication status. + * + * In particular, [[User::isGuest]] returns a value indicating whether the current user is a guest or not. + * Through methods [[login()]] and [[logout()]], you can change the user authentication status. + * + * User works with a class implementing the [[Identity]] interface. This class implements + * the actual user authentication logic and is often backed by a user database table. + * * @author Qiang Xue * @since 2.0 */ diff --git a/framework/web/UserEvent.php b/framework/web/UserEvent.php index 6955ae5..7a5d23d 100644 --- a/framework/web/UserEvent.php +++ b/framework/web/UserEvent.php @@ -30,5 +30,5 @@ class UserEvent extends Event * Event handlers may modify this property to determine whether the login or logout should proceed. * This property is only meaningful for [[User::EVENT_BEFORE_LOGIN]] and [[User::EVENT_BEFORE_LOGOUT]] events. */ - public $isValid; + public $isValid = true; } \ No newline at end of file From 4f2a69e9e1612e3fea4ae10eeda82733bfdc95b8 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Fri, 29 Mar 2013 08:26:04 -0400 Subject: [PATCH 006/104] Added IP to log result. --- framework/logging/Target.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/framework/logging/Target.php b/framework/logging/Target.php index b88e78d..e76e8ac 100644 --- a/framework/logging/Target.php +++ b/framework/logging/Target.php @@ -238,6 +238,7 @@ abstract class Target extends \yii\base\Component if (!is_string($text)) { $text = var_export($text, true); } - return date('Y/m/d H:i:s', $timestamp) . " [$level] [$category] $text\n"; + $ip = isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '127.0.0.1'; + return date('Y/m/d H:i:s', $timestamp) . " [$ip] [$level] [$category] $text\n"; } } From 00319fb898ea095c9ccb79901d661cd1678c04e1 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Fri, 29 Mar 2013 08:32:58 -0400 Subject: [PATCH 007/104] Fixed about previous exception. --- framework/base/HttpException.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/framework/base/HttpException.php b/framework/base/HttpException.php index 94a9a55..948d96b 100644 --- a/framework/base/HttpException.php +++ b/framework/base/HttpException.php @@ -29,11 +29,12 @@ class HttpException extends UserException * @param integer $status HTTP status code, such as 404, 500, etc. * @param string $message error message * @param integer $code error code + * @param \Exception $previous The previous exception used for the exception chaining. */ - public function __construct($status, $message = null, $code = 0) + public function __construct($status, $message = null, $code = 0, \Exception $previous = null) { $this->statusCode = $status; - parent::__construct($message, $code); + parent::__construct($message, $code, $previous); } /** From 9da81894be1b0268eab2216b44c4bc48e5e24fd5 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Fri, 29 Mar 2013 18:23:50 +0400 Subject: [PATCH 008/104] moved helper test from util namespace to helpers namespace, added tests for StringHelper --- tests/unit/framework/helpers/ArrayHelperTest.php | 50 +++ tests/unit/framework/helpers/HtmlTest.php | 448 ++++++++++++++++++++++ tests/unit/framework/helpers/StringHelperTest.php | 73 ++++ tests/unit/framework/util/ArrayHelperTest.php | 50 --- tests/unit/framework/util/HtmlTest.php | 448 ---------------------- 5 files changed, 571 insertions(+), 498 deletions(-) create mode 100644 tests/unit/framework/helpers/ArrayHelperTest.php create mode 100644 tests/unit/framework/helpers/HtmlTest.php create mode 100644 tests/unit/framework/helpers/StringHelperTest.php delete mode 100644 tests/unit/framework/util/ArrayHelperTest.php delete mode 100644 tests/unit/framework/util/HtmlTest.php diff --git a/tests/unit/framework/helpers/ArrayHelperTest.php b/tests/unit/framework/helpers/ArrayHelperTest.php new file mode 100644 index 0000000..187217f --- /dev/null +++ b/tests/unit/framework/helpers/ArrayHelperTest.php @@ -0,0 +1,50 @@ + 'b', 'age' => 3), + array('name' => 'a', 'age' => 1), + array('name' => 'c', 'age' => 2), + ); + ArrayHelper::multisort($array, 'name'); + $this->assertEquals(array('name' => 'a', 'age' => 1), $array[0]); + $this->assertEquals(array('name' => 'b', 'age' => 3), $array[1]); + $this->assertEquals(array('name' => 'c', 'age' => 2), $array[2]); + + // multiple keys + $array = array( + array('name' => 'b', 'age' => 3), + array('name' => 'a', 'age' => 2), + array('name' => 'a', 'age' => 1), + ); + ArrayHelper::multisort($array, array('name', 'age')); + $this->assertEquals(array('name' => 'a', 'age' => 1), $array[0]); + $this->assertEquals(array('name' => 'a', 'age' => 2), $array[1]); + $this->assertEquals(array('name' => 'b', 'age' => 3), $array[2]); + + // case-insensitive + $array = array( + array('name' => 'a', 'age' => 3), + array('name' => 'b', 'age' => 2), + array('name' => 'A', 'age' => 1), + ); + ArrayHelper::multisort($array, array('name', 'age'), SORT_ASC, array(SORT_STRING|SORT_FLAG_CASE, SORT_REGULAR)); + $this->assertEquals(array('name' => 'A', 'age' => 1), $array[0]); + $this->assertEquals(array('name' => 'a', 'age' => 3), $array[1]); + $this->assertEquals(array('name' => 'b', 'age' => 2), $array[2]); + } +} diff --git a/tests/unit/framework/helpers/HtmlTest.php b/tests/unit/framework/helpers/HtmlTest.php new file mode 100644 index 0000000..2c3de72 --- /dev/null +++ b/tests/unit/framework/helpers/HtmlTest.php @@ -0,0 +1,448 @@ + 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; + + $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() + { + $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() + { + $this->assertEquals('', Html::buttonInput('test')); + $this->assertEquals('', Html::buttonInput('test', 'text', array('class' => 'a'))); + } + + public function testSubmitInput() + { + $this->assertEquals('', Html::submitInput()); + $this->assertEquals('', Html::submitInput('test', 'text', array('class' => 'a'))); + } + + public function testResetInput() + { + $this->assertEquals('', Html::resetInput()); + $this->assertEquals('', Html::resetInput('test', 'text', array('class' => 'a'))); + } + + public function testTextInput() + { + $this->assertEquals('', Html::textInput('test')); + $this->assertEquals('', Html::textInput('test', 'value', array('class' => 't'))); + } + + public function testHiddenInput() + { + $this->assertEquals('', Html::hiddenInput('test')); + $this->assertEquals('', Html::hiddenInput('test', 'value', array('class' => 't'))); + } + + public function testPasswordInput() + { + $this->assertEquals('', Html::passwordInput('test')); + $this->assertEquals('', Html::passwordInput('test', 'value', array('class' => 't'))); + } + + public function testFileInput() + { + $this->assertEquals('', Html::fileInput('test')); + $this->assertEquals('', Html::fileInput('test', 'value', array('class' => 't'))); + } + + public function testTextarea() + { + $this->assertEquals('', Html::textarea('test')); + $this->assertEquals('', Html::textarea('test', 'value<>', array('class' => 't'))); + } + + public function testRadio() + { + $this->assertEquals('', Html::radio('test')); + $this->assertEquals('', Html::radio('test', true, null, array('class' => 'a'))); + $this->assertEquals('', Html::radio('test', true, 2, array('class' => 'a' , 'uncheck' => '0'))); + } + + public function testCheckbox() + { + $this->assertEquals('', Html::checkbox('test')); + $this->assertEquals('', Html::checkbox('test', true, null, array('class' => 'a'))); + $this->assertEquals('', Html::checkbox('test', true, 2, array('class' => 'a', 'uncheck' => '0'))); + } + + public function testDropDownList() + { + $expected = << + + +EOD; + $this->assertEquals($expected, Html::dropDownList('test')); + $expected = << + + + +EOD; + $this->assertEquals($expected, Html::dropDownList('test', null, $this->getDataItems())); + $expected = << + + + +EOD; + $this->assertEquals($expected, Html::dropDownList('test', 'value2', $this->getDataItems())); + } + + public function testListBox() + { + $expected = << + + +EOD; + $this->assertEquals($expected, Html::listBox('test')); + $expected = << + + + +EOD; + $this->assertEquals($expected, Html::listBox('test', null, $this->getDataItems(), array('size' => 5))); + $expected = << + + + +EOD; + $this->assertEquals($expected, Html::listBox('test', null, $this->getDataItems2())); + $expected = << + + + +EOD; + $this->assertEquals($expected, Html::listBox('test', 'value2', $this->getDataItems())); + $expected = << + + + +EOD; + $this->assertEquals($expected, Html::listBox('test', array('value1', 'value2'), $this->getDataItems())); + + $expected = << + + +EOD; + $this->assertEquals($expected, Html::listBox('test', null, array(), array('multiple' => true))); + $expected = << +EOD; + $this->assertEquals($expected, Html::listBox('test', '', array(), array('unselect' => '0'))); + } + + public function testCheckboxList() + { + $this->assertEquals('', Html::checkboxList('test')); + + $expected = << text1 + +EOD; + $this->assertEquals($expected, Html::checkboxList('test', array('value2'), $this->getDataItems())); + + $expected = << text1<> + +EOD; + $this->assertEquals($expected, Html::checkboxList('test', array('value2'), $this->getDataItems2())); + + $expected = <<
    + +EOD; + $this->assertEquals($expected, Html::checkboxList('test', array('value2'), $this->getDataItems(), array( + 'separator' => "
    \n", + 'unselect' => '0', + ))); + + $expected = <<text1 +1 +EOD; + $this->assertEquals($expected, Html::checkboxList('test', array('value2'), $this->getDataItems(), array( + 'item' => function ($index, $label, $name, $checked, $value) { + return $index . Html::label($label . ' ' . Html::checkbox($name, $checked, $value)); + } + ))); + } + + public function testRadioList() + { + $this->assertEquals('', Html::radioList('test')); + + $expected = << text1 + +EOD; + $this->assertEquals($expected, Html::radioList('test', array('value2'), $this->getDataItems())); + + $expected = << text1<> + +EOD; + $this->assertEquals($expected, Html::radioList('test', array('value2'), $this->getDataItems2())); + + $expected = <<
    + +EOD; + $this->assertEquals($expected, Html::radioList('test', array('value2'), $this->getDataItems(), array( + 'separator' => "
    \n", + 'unselect' => '0', + ))); + + $expected = <<text1 +1 +EOD; + $this->assertEquals($expected, Html::radioList('test', array('value2'), $this->getDataItems(), array( + 'item' => function ($index, $label, $name, $checked, $value) { + return $index . Html::label($label . ' ' . Html::radio($name, $checked, $value)); + } + ))); + } + + public function testRenderOptions() + { + $data = array( + 'value1' => 'label1', + 'group1' => array( + 'value11' => 'label11', + 'group11' => array( + 'value111' => 'label111', + ), + 'group12' => array(), + ), + 'value2' => 'label2', + 'group2' => array(), + ); + $expected = <<please select<> + + + + + + + + + + + + + + +EOD; + $attributes = array( + 'prompt' => 'please select<>', + 'options' => array( + 'value111' => array('class' => 'option'), + ), + 'groups' => array( + 'group12' => array('class' => 'group'), + ), + ); + $this->assertEquals($expected, Html::renderSelectOptions(array('value111', 'value1'), $data, $attributes)); + } + + public function testRenderAttributes() + { + $this->assertEquals('', Html::renderTagAttributes(array())); + $this->assertEquals(' name="test" value="1<>"', Html::renderTagAttributes(array('name' => 'test', 'empty' => null, 'value' => '1<>'))); + Html::$showBooleanAttributeValues = false; + $this->assertEquals(' checked disabled', Html::renderTagAttributes(array('checked' => 'checked', 'disabled' => true, 'hidden' => false))); + Html::$showBooleanAttributeValues = true; + } + + protected function getDataItems() + { + return array( + 'value1' => 'text1', + 'value2' => 'text2', + ); + } + + protected function getDataItems2() + { + return array( + 'value1<>' => 'text1<>', + 'value 2' => 'text 2', + ); + } +} diff --git a/tests/unit/framework/helpers/StringHelperTest.php b/tests/unit/framework/helpers/StringHelperTest.php new file mode 100644 index 0000000..4e1266f --- /dev/null +++ b/tests/unit/framework/helpers/StringHelperTest.php @@ -0,0 +1,73 @@ +assertEquals(4, StringHelper::strlen('this')); + $this->assertEquals(6, StringHelper::strlen('это')); + } + + public function testSubstr() + { + $this->assertEquals('th', StringHelper::substr('this', 0, 2)); + $this->assertEquals('э', StringHelper::substr('это', 0, 2)); + } + + public function testPluralize() + { + $testData = array( + 'move' => 'moves', + 'foot' => 'feet', + 'child' => 'children', + 'human' => 'humans', + 'man' => 'men', + 'staff' => 'staff', + 'tooth' => 'teeth', + 'person' => 'people', + 'mouse' => 'mice', + 'touch' => 'touches', + 'hash' => 'hashes', + 'shelf' => 'shelves', + 'potato' => 'potatoes', + 'bus' => 'buses', + 'test' => 'tests', + 'car' => 'cars', + ); + + foreach($testData as $testIn => $testOut) { + $this->assertEquals($testOut, StringHelper::pluralize($testIn)); + $this->assertEquals(ucfirst($testOut), ucfirst(StringHelper::pluralize($testIn))); + } + } + + public function testCamel2words() + { + $this->assertEquals('Camel Case', StringHelper::camel2words('camelCase')); + $this->assertEquals('Lower Case', StringHelper::camel2words('lower_case')); + $this->assertEquals('Tricky Stuff It Is Testing', StringHelper::camel2words(' tricky_stuff.it-is testing... ')); + } + + public function testCamel2id() + { + $this->assertEquals('post-tag', StringHelper::camel2id('PostTag')); + $this->assertEquals('post_tag', StringHelper::camel2id('PostTag', '_')); + + $this->assertEquals('post-tag', StringHelper::camel2id('postTag')); + $this->assertEquals('post_tag', StringHelper::camel2id('postTag', '_')); + } + + public function testId2camel() + { + $this->assertEquals('PostTag', StringHelper::id2camel('post-tag')); + $this->assertEquals('PostTag', StringHelper::id2camel('post_tag', '_')); + + $this->assertEquals('PostTag', StringHelper::id2camel('post-tag')); + $this->assertEquals('PostTag', StringHelper::id2camel('post_tag', '_')); + } +} \ No newline at end of file diff --git a/tests/unit/framework/util/ArrayHelperTest.php b/tests/unit/framework/util/ArrayHelperTest.php deleted file mode 100644 index 117c702..0000000 --- a/tests/unit/framework/util/ArrayHelperTest.php +++ /dev/null @@ -1,50 +0,0 @@ - 'b', 'age' => 3), - array('name' => 'a', 'age' => 1), - array('name' => 'c', 'age' => 2), - ); - ArrayHelper::multisort($array, 'name'); - $this->assertEquals(array('name' => 'a', 'age' => 1), $array[0]); - $this->assertEquals(array('name' => 'b', 'age' => 3), $array[1]); - $this->assertEquals(array('name' => 'c', 'age' => 2), $array[2]); - - // multiple keys - $array = array( - array('name' => 'b', 'age' => 3), - array('name' => 'a', 'age' => 2), - array('name' => 'a', 'age' => 1), - ); - ArrayHelper::multisort($array, array('name', 'age')); - $this->assertEquals(array('name' => 'a', 'age' => 1), $array[0]); - $this->assertEquals(array('name' => 'a', 'age' => 2), $array[1]); - $this->assertEquals(array('name' => 'b', 'age' => 3), $array[2]); - - // case-insensitive - $array = array( - array('name' => 'a', 'age' => 3), - array('name' => 'b', 'age' => 2), - array('name' => 'A', 'age' => 1), - ); - ArrayHelper::multisort($array, array('name', 'age'), SORT_ASC, array(SORT_STRING|SORT_FLAG_CASE, SORT_REGULAR)); - $this->assertEquals(array('name' => 'A', 'age' => 1), $array[0]); - $this->assertEquals(array('name' => 'a', 'age' => 3), $array[1]); - $this->assertEquals(array('name' => 'b', 'age' => 2), $array[2]); - } -} diff --git a/tests/unit/framework/util/HtmlTest.php b/tests/unit/framework/util/HtmlTest.php deleted file mode 100644 index eba1a20..0000000 --- a/tests/unit/framework/util/HtmlTest.php +++ /dev/null @@ -1,448 +0,0 @@ - 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; - - $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() - { - $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() - { - $this->assertEquals('', Html::buttonInput('test')); - $this->assertEquals('', Html::buttonInput('test', 'text', array('class' => 'a'))); - } - - public function testSubmitInput() - { - $this->assertEquals('', Html::submitInput()); - $this->assertEquals('', Html::submitInput('test', 'text', array('class' => 'a'))); - } - - public function testResetInput() - { - $this->assertEquals('', Html::resetInput()); - $this->assertEquals('', Html::resetInput('test', 'text', array('class' => 'a'))); - } - - public function testTextInput() - { - $this->assertEquals('', Html::textInput('test')); - $this->assertEquals('', Html::textInput('test', 'value', array('class' => 't'))); - } - - public function testHiddenInput() - { - $this->assertEquals('', Html::hiddenInput('test')); - $this->assertEquals('', Html::hiddenInput('test', 'value', array('class' => 't'))); - } - - public function testPasswordInput() - { - $this->assertEquals('', Html::passwordInput('test')); - $this->assertEquals('', Html::passwordInput('test', 'value', array('class' => 't'))); - } - - public function testFileInput() - { - $this->assertEquals('', Html::fileInput('test')); - $this->assertEquals('', Html::fileInput('test', 'value', array('class' => 't'))); - } - - public function testTextarea() - { - $this->assertEquals('', Html::textarea('test')); - $this->assertEquals('', Html::textarea('test', 'value<>', array('class' => 't'))); - } - - public function testRadio() - { - $this->assertEquals('', Html::radio('test')); - $this->assertEquals('', Html::radio('test', true, null, array('class' => 'a'))); - $this->assertEquals('', Html::radio('test', true, 2, array('class' => 'a' , 'uncheck' => '0'))); - } - - public function testCheckbox() - { - $this->assertEquals('', Html::checkbox('test')); - $this->assertEquals('', Html::checkbox('test', true, null, array('class' => 'a'))); - $this->assertEquals('', Html::checkbox('test', true, 2, array('class' => 'a', 'uncheck' => '0'))); - } - - public function testDropDownList() - { - $expected = << - - -EOD; - $this->assertEquals($expected, Html::dropDownList('test')); - $expected = << - - - -EOD; - $this->assertEquals($expected, Html::dropDownList('test', null, $this->getDataItems())); - $expected = << - - - -EOD; - $this->assertEquals($expected, Html::dropDownList('test', 'value2', $this->getDataItems())); - } - - public function testListBox() - { - $expected = << - - -EOD; - $this->assertEquals($expected, Html::listBox('test')); - $expected = << - - - -EOD; - $this->assertEquals($expected, Html::listBox('test', null, $this->getDataItems(), array('size' => 5))); - $expected = << - - - -EOD; - $this->assertEquals($expected, Html::listBox('test', null, $this->getDataItems2())); - $expected = << - - - -EOD; - $this->assertEquals($expected, Html::listBox('test', 'value2', $this->getDataItems())); - $expected = << - - - -EOD; - $this->assertEquals($expected, Html::listBox('test', array('value1', 'value2'), $this->getDataItems())); - - $expected = << - - -EOD; - $this->assertEquals($expected, Html::listBox('test', null, array(), array('multiple' => true))); - $expected = << -EOD; - $this->assertEquals($expected, Html::listBox('test', '', array(), array('unselect' => '0'))); - } - - public function testCheckboxList() - { - $this->assertEquals('', Html::checkboxList('test')); - - $expected = << text1 - -EOD; - $this->assertEquals($expected, Html::checkboxList('test', array('value2'), $this->getDataItems())); - - $expected = << text1<> - -EOD; - $this->assertEquals($expected, Html::checkboxList('test', array('value2'), $this->getDataItems2())); - - $expected = <<
    - -EOD; - $this->assertEquals($expected, Html::checkboxList('test', array('value2'), $this->getDataItems(), array( - 'separator' => "
    \n", - 'unselect' => '0', - ))); - - $expected = <<text1 -1 -EOD; - $this->assertEquals($expected, Html::checkboxList('test', array('value2'), $this->getDataItems(), array( - 'item' => function ($index, $label, $name, $checked, $value) { - return $index . Html::label($label . ' ' . Html::checkbox($name, $checked, $value)); - } - ))); - } - - public function testRadioList() - { - $this->assertEquals('', Html::radioList('test')); - - $expected = << text1 - -EOD; - $this->assertEquals($expected, Html::radioList('test', array('value2'), $this->getDataItems())); - - $expected = << text1<> - -EOD; - $this->assertEquals($expected, Html::radioList('test', array('value2'), $this->getDataItems2())); - - $expected = <<
    - -EOD; - $this->assertEquals($expected, Html::radioList('test', array('value2'), $this->getDataItems(), array( - 'separator' => "
    \n", - 'unselect' => '0', - ))); - - $expected = <<text1 -1 -EOD; - $this->assertEquals($expected, Html::radioList('test', array('value2'), $this->getDataItems(), array( - 'item' => function ($index, $label, $name, $checked, $value) { - return $index . Html::label($label . ' ' . Html::radio($name, $checked, $value)); - } - ))); - } - - public function testRenderOptions() - { - $data = array( - 'value1' => 'label1', - 'group1' => array( - 'value11' => 'label11', - 'group11' => array( - 'value111' => 'label111', - ), - 'group12' => array(), - ), - 'value2' => 'label2', - 'group2' => array(), - ); - $expected = <<please select<> - - - - - - - - - - - - - - -EOD; - $attributes = array( - 'prompt' => 'please select<>', - 'options' => array( - 'value111' => array('class' => 'option'), - ), - 'groups' => array( - 'group12' => array('class' => 'group'), - ), - ); - $this->assertEquals($expected, Html::renderSelectOptions(array('value111', 'value1'), $data, $attributes)); - } - - public function testRenderAttributes() - { - $this->assertEquals('', Html::renderTagAttributes(array())); - $this->assertEquals(' name="test" value="1<>"', Html::renderTagAttributes(array('name' => 'test', 'empty' => null, 'value' => '1<>'))); - Html::$showBooleanAttributeValues = false; - $this->assertEquals(' checked disabled', Html::renderTagAttributes(array('checked' => 'checked', 'disabled' => true, 'hidden' => false))); - Html::$showBooleanAttributeValues = true; - } - - protected function getDataItems() - { - return array( - 'value1' => 'text1', - 'value2' => 'text2', - ); - } - - protected function getDataItems2() - { - return array( - 'value1<>' => 'text1<>', - 'value 2' => 'text 2', - ); - } -} From f69a73baf2bf377fbba09268335aec8228c35413 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Fri, 29 Mar 2013 13:31:08 -0400 Subject: [PATCH 009/104] refactored User and Identity classes. --- framework/web/Identity.php | 36 +++++++++++++- framework/web/User.php | 115 ++++++++++++++++++++++++--------------------- 2 files changed, 96 insertions(+), 55 deletions(-) diff --git a/framework/web/Identity.php b/framework/web/Identity.php index 89d4282..6d67bc0 100644 --- a/framework/web/Identity.php +++ b/framework/web/Identity.php @@ -8,6 +8,35 @@ namespace yii\web; /** + * Identity is the interface that should be implemented by a class providing identity information. + * + * This interface can typically be implemented by a user model class. For example, the following + * code shows how to implement this interface by a User ActiveRecord class: + * + * ~~~ + * class User extends ActiveRecord implements Identity + * { + * public static function findIdentity($id) + * { + * return static::find($id); + * } + * + * public function getId() + * { + * return $this->id; + * } + * + * public function getAuthKey() + * { + * return $this->authKey; + * } + * + * public function validateAuthKey($authKey) + * { + * return $this->authKey === $authKey; + * } + * } + * ~~~ * * @author Qiang Xue * @since 2.0 @@ -29,8 +58,11 @@ interface Identity public function getId(); /** * Returns a key that can be used to check the validity of a given identity ID. - * The space of such keys should be big and random enough to defeat potential identity attacks. - * The returned key can be a string, an integer, or any serializable data. + * + * The key should be unique for each individual user, and should be persistent + * so that it can be used to check the validity of the user identity. + * + * The space of such keys should be big enough to defeat potential identity attacks. * * This is required if [[User::enableAutoLogin]] is enabled. * @return string a key that is used to check the validity of a given identity ID. diff --git a/framework/web/User.php b/framework/web/User.php index 5b5f977..4dc2607 100644 --- a/framework/web/User.php +++ b/framework/web/User.php @@ -13,7 +13,7 @@ use yii\base\HttpException; use yii\base\InvalidConfigException; /** - * User is an application component that manages the user authentication status. + * User is the class for the "user" application component that manages the user authentication status. * * In particular, [[User::isGuest]] returns a value indicating whether the current user is a guest or not. * Through methods [[login()]] and [[logout()]], you can change the user authentication status. @@ -32,15 +32,6 @@ class User extends Component const EVENT_AFTER_LOGOUT = 'afterLogout'; /** - * @var Identity the identity object associated with the currently logged user. - * This property is set automatically be the User component. Do not modify it directly - * unless you understand the consequence. You should normally use [[login()]], [[logout()]], - * or [[switchIdentity()]] to update the identity associated with the current user. - * - * If this property is null, it means the current user is a guest (not authenticated). - */ - public $identity; - /** * @var string the class name of the [[identity]] object. */ public $identityClass; @@ -65,7 +56,7 @@ class User extends Component * @var array the configuration of the identity cookie. This property is used only when [[enableAutoLogin]] is true. * @see Cookie */ - public $identityCookie = array('name' => '__identity'); + public $identityCookie = array('name' => '__identity', 'httponly' => true); /** * @var integer the number of seconds in which the user will be logged out automatically if he * remains inactive. If this property is not set, the user will be logged out after @@ -112,8 +103,6 @@ class User extends Component Yii::$app->getSession()->open(); - $this->loadIdentity(); - $this->renewAuthStatus(); if ($this->enableAutoLogin) { @@ -125,19 +114,43 @@ class User extends Component } } + private $_identity = false; + /** - * Loads the [[identity]] object according to [[id]]. + * Returns the identity object associated with the currently logged user. + * @return Identity the identity object associated with the currently logged user. + * Null is returned if the user is not logged in (not authenticated). + * @see login + * @see logout */ - protected function loadIdentity() + public function getIdentity() { - $id = $this->getId(); - if ($id === null) { - $this->identity = null; - } else { - /** @var $class Identity */ - $class = $this->identityClass; - $this->identity = $class::findIdentity($id); + if ($this->_identity === false) { + $id = $this->getId(); + if ($id === null) { + $this->_identity = null; + } else { + /** @var $class Identity */ + $class = $this->identityClass; + $this->_identity = $class::findIdentity($id); + } } + return $this->_identity; + } + + /** + * Sets the identity object. + * This method should be mainly be used by the User component or its child class + * to maintain the identity object. + * + * You should normally update the user identity via methods [[login()]], [[logout()]] + * or [[switchIdentity()]]. + * + * @param Identity $identity the identity object associated with the currently logged user. + */ + public function setIdentity($identity) + { + $this->_identity = $identity; } /** @@ -157,10 +170,7 @@ class User extends Component public function login($identity, $duration = 0) { if ($this->beforeLogin($identity, false)) { - $this->switchIdentity($identity); - if ($duration > 0 && $this->enableAutoLogin) { - $this->sendIdentityCookie($identity, $duration); - } + $this->switchIdentity($identity, $duration); $this->afterLogin($identity, false); } return !$this->getIsGuest(); @@ -185,10 +195,7 @@ class User extends Component $identity = $class::findIdentity($id); if ($identity !== null && $identity->validateAuthKey($authKey)) { if ($this->beforeLogin($identity, true)) { - $this->switchIdentity($identity); - if ($this->autoRenewCookie) { - $this->sendIdentityCookie($identity, $duration); - } + $this->switchIdentity($identity, $this->autoRenewCookie ? $duration : 0); $this->afterLogin($identity, true); } } elseif ($identity !== null) { @@ -206,12 +213,9 @@ class User extends Component */ public function logout($destroySession = true) { - $identity = $this->identity; + $identity = $this->getIdentity(); if ($identity !== null && $this->beforeLogout($identity)) { $this->switchIdentity(null); - if ($this->enableAutoLogin) { - Yii::$app->getResponse()->getCookies()->remove(new Cookie($this->identityCookie)); - } if ($destroySession) { Yii::$app->getSession()->destroy(); } @@ -225,7 +229,7 @@ class User extends Component */ public function getIsGuest() { - return $this->identity === null; + return $this->getIdentity() === null; } /** @@ -238,14 +242,6 @@ class User extends Component } /** - * @param string|integer $value the unique identifier for the user. If null, it means the user is a guest. - */ - public function setId($value) - { - Yii::$app->getSession()->set($this->idVar, $value); - } - - /** * Returns the URL that the user should be redirected to after successful login. * This property is usually used by the login action. If the login is successful, * the action should read this property and use it to redirect the user browser. @@ -400,24 +396,37 @@ class User extends Component } /** - * Changes the current user with the specified identity information. - * This method is called by [[login()]] and [[loginByCookie()]] - * when the current user needs to be associated with the corresponding - * identity information. + * Switches to a new identity for the current user. + * + * This method will save necessary session information to keep track of the user authentication status. + * If `$duration` is provided, it will also send out appropriate identity cookie + * to support cookie-based login. + * + * This method is mainly called by [[login()]], [[logout()]] and [[loginByCookie()]] + * when the current user needs to be associated with the corresponding identity information. + * * @param Identity $identity the identity information to be associated with the current user. + * If null, it means switching to be a guest. + * @param integer $duration number of seconds that the user can remain in logged-in status. + * This parameter is used only when `$identity` is not null. */ - protected function switchIdentity($identity) + public function switchIdentity($identity, $duration = 0) { - Yii::$app->getSession()->regenerateID(true); - $this->identity = $identity; $session = Yii::$app->getSession(); + $session->regenerateID(true); + $this->setIdentity($identity); $session->remove($this->idVar); $session->remove($this->authTimeoutVar); if ($identity instanceof Identity) { - $this->setId($identity->getId()); + $session->set($this->idVar, $identity->getId()); if ($this->authTimeout !== null) { - Yii::$app->getSession()->set($this->authTimeoutVar, time() + $this->authTimeout); + $session->set($this->authTimeoutVar, time() + $this->authTimeout); + } + if ($duration > 0 && $this->enableAutoLogin) { + $this->sendIdentityCookie($identity, $duration); } + } elseif ($this->enableAutoLogin) { + Yii::$app->getResponse()->getCookies()->remove(new Cookie($this->identityCookie)); } } @@ -429,7 +438,7 @@ class User extends Component */ protected function renewAuthStatus() { - if ($this->authTimeout !== null && $this->identity !== null) { + if ($this->authTimeout !== null && !$this->getIsGuest()) { $expire = Yii::$app->getSession()->get($this->authTimeoutVar); if ($expire !== null && $expire < time()) { $this->logout(false); From 92e634db66272b6151392dbef74c1a40baa8437c Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Fri, 29 Mar 2013 22:54:45 +0400 Subject: [PATCH 010/104] Ability to configure session cookie, httponly by default --- framework/web/Session.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/framework/web/Session.php b/framework/web/Session.php index 3e0f599..4c0505f 100644 --- a/framework/web/Session.php +++ b/framework/web/Session.php @@ -60,6 +60,13 @@ class Session extends Component implements \IteratorAggregate, \ArrayAccess, \Co public $flashVar = '__flash'; /** + * @var array parameter-value pairs to override default session cookie parameters + */ + public $cookieParams = array( + 'httponly' => true + ); + + /** * Initializes the application component. * This method is required by IApplicationComponent and is invoked by application. */ @@ -111,6 +118,8 @@ class Session extends Component implements \IteratorAggregate, \ArrayAccess, \Co ); } + $this->setCookieParams($this->cookieParams); + @session_start(); if (session_id() == '') { From 7d27d65a136ec85fe8d7331444fd35fcd0f1460e Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Fri, 29 Mar 2013 15:04:15 -0400 Subject: [PATCH 011/104] bug fix. --- framework/YiiBase.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework/YiiBase.php b/framework/YiiBase.php index 16e237d..261a99e 100644 --- a/framework/YiiBase.php +++ b/framework/YiiBase.php @@ -238,7 +238,7 @@ class YiiBase { if ($path === null) { unset(self::$aliases[$alias]); - } elseif ($path[0] !== '@') { + } elseif (strncmp($path, '@', 1)) { self::$aliases[$alias] = rtrim($path, '\\/'); } else { self::$aliases[$alias] = static::getAlias($path); From 6a2bfae41c32f330facf8df516423e450b2003fc Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Fri, 29 Mar 2013 23:32:34 -0400 Subject: [PATCH 012/104] use parameter binding for query builder. --- framework/db/Command.php | 2 +- framework/db/Connection.php | 14 +- framework/db/QueryBuilder.php | 469 ++++++++++++++++++++++-------------------- 3 files changed, 251 insertions(+), 234 deletions(-) diff --git a/framework/db/Command.php b/framework/db/Command.php index a30aa14..2a13a89 100644 --- a/framework/db/Command.php +++ b/framework/db/Command.php @@ -543,7 +543,7 @@ class Command extends \yii\base\Component */ public function delete($table, $condition = '', $params = array()) { - $sql = $this->db->getQueryBuilder()->delete($table, $condition); + $sql = $this->db->getQueryBuilder()->delete($table, $condition, $params); return $this->setSql($sql)->bindValues($params); } diff --git a/framework/db/Connection.php b/framework/db/Connection.php index e84970b..1be43eb 100644 --- a/framework/db/Connection.php +++ b/framework/db/Connection.php @@ -248,15 +248,15 @@ class Connection extends Component * [[Schema]] class to support DBMS that is not supported by Yii. */ public $schemaMap = array( - 'pgsql' => 'yii\db\pgsql\Schema', // PostgreSQL - 'mysqli' => 'yii\db\mysql\Schema', // MySQL - 'mysql' => 'yii\db\mysql\Schema', // MySQL - 'sqlite' => 'yii\db\sqlite\Schema', // sqlite 3 + 'pgsql' => 'yii\db\pgsql\Schema', // PostgreSQL + 'mysqli' => 'yii\db\mysql\Schema', // MySQL + 'mysql' => 'yii\db\mysql\Schema', // MySQL + 'sqlite' => 'yii\db\sqlite\Schema', // sqlite 3 'sqlite2' => 'yii\db\sqlite\Schema', // sqlite 2 'mssql' => 'yi\db\dao\mssql\Schema', // Mssql driver on windows hosts - 'dblib' => 'yii\db\mssql\Schema', // dblib drivers on linux (and maybe others os) hosts - 'sqlsrv' => 'yii\db\mssql\Schema', // Mssql - 'oci' => 'yii\db\oci\Schema', // Oracle driver + 'sqlsrv' => 'yii\db\mssql\Schema', // Mssql + 'oci' => 'yii\db\oci\Schema', // Oracle driver + 'dblib' => 'yii\db\mssql\Schema', // dblib drivers on linux (and maybe others os) hosts ); /** * @var Transaction the currently active transaction diff --git a/framework/db/QueryBuilder.php b/framework/db/QueryBuilder.php index 75375cc..62ef58f 100644 --- a/framework/db/QueryBuilder.php +++ b/framework/db/QueryBuilder.php @@ -22,6 +22,11 @@ use yii\base\NotSupportedException; class QueryBuilder extends \yii\base\Object { /** + * The prefix for automatically generated query binding parameters. + */ + const PARAM_PREFIX = ':qp'; + + /** * @var Connection the database connection. */ public $db; @@ -58,11 +63,11 @@ class QueryBuilder extends \yii\base\Object $clauses = array( $this->buildSelect($query->select, $query->distinct, $query->selectOption), $this->buildFrom($query->from), - $this->buildJoin($query->join), - $this->buildWhere($query->where), + $this->buildJoin($query->join, $query->params), + $this->buildWhere($query->where, $query->params), $this->buildGroupBy($query->groupBy), - $this->buildHaving($query->having), - $this->buildUnion($query->union), + $this->buildHaving($query->having, $query->params), + $this->buildUnion($query->union, $query->params), $this->buildOrderBy($query->orderBy), $this->buildLimit($query->limit, $query->offset), ); @@ -92,7 +97,6 @@ class QueryBuilder extends \yii\base\Object { $names = array(); $placeholders = array(); - $count = 0; foreach ($columns as $name => $value) { $names[] = $this->db->quoteColumnName($name); if ($value instanceof Expression) { @@ -101,9 +105,9 @@ class QueryBuilder extends \yii\base\Object $params[$n] = $v; } } else { - $placeholders[] = ':p' . $count; - $params[':p' . $count] = $value; - $count++; + $phName = self::PARAM_PREFIX . count($params); + $placeholders[] = $phName; + $params[$phName] = $value; } } @@ -159,10 +163,9 @@ class QueryBuilder extends \yii\base\Object * so that they can be bound to the DB command later. * @return string the UPDATE SQL */ - public function update($table, $columns, $condition = '', &$params) + public function update($table, $columns, $condition, &$params) { $lines = array(); - $count = 0; foreach ($columns as $name => $value) { if ($value instanceof Expression) { $lines[] = $this->db->quoteColumnName($name) . '=' . $value->expression; @@ -170,17 +173,15 @@ class QueryBuilder extends \yii\base\Object $params[$n] = $v; } } else { - $lines[] = $this->db->quoteColumnName($name) . '=:p' . $count; - $params[':p' . $count] = $value; - $count++; + $phName = self::PARAM_PREFIX . count($params); + $lines[] = $this->db->quoteColumnName($name) . '=' . $phName; + $params[$phName] = $value; } } - $sql = 'UPDATE ' . $this->db->quoteTableName($table) . ' SET ' . implode(', ', $lines); - if (($where = $this->buildCondition($condition)) !== '') { - $sql .= ' WHERE ' . $where; - } - return $sql; + $sql = 'UPDATE ' . $this->db->quoteTableName($table) . ' SET ' . implode(', ', $lines); + $where = $this->buildWhere($condition, $params); + return $where === '' ? $sql : $sql . ' ' . $where; } /** @@ -196,15 +197,15 @@ class QueryBuilder extends \yii\base\Object * @param string $table the table where the data will be deleted from. * @param mixed $condition the condition that will be put in the WHERE part. Please * refer to [[Query::where()]] on how to specify condition. + * @param array $params the binding parameters that will be modified by this method + * so that they can be bound to the DB command later. * @return string the DELETE SQL */ - public function delete($table, $condition = '') + public function delete($table, $condition, &$params) { $sql = 'DELETE FROM ' . $this->db->quoteTableName($table); - if (($where = $this->buildCondition($condition)) !== '') { - $sql .= ' WHERE ' . $where; - } - return $sql; + $where = $this->buildWhere($condition, $params); + return $where === '' ? $sql : $sql . ' ' . $where; } /** @@ -479,200 +480,6 @@ class QueryBuilder extends \yii\base\Object } /** - * Parses the condition specification and generates the corresponding SQL expression. - * @param string|array $condition the condition specification. Please refer to [[Query::where()]] - * on how to specify a condition. - * @return string the generated SQL expression - * @throws \yii\db\Exception if the condition is in bad format - */ - public function buildCondition($condition) - { - static $builders = array( - 'AND' => 'buildAndCondition', - 'OR' => 'buildAndCondition', - 'BETWEEN' => 'buildBetweenCondition', - 'NOT BETWEEN' => 'buildBetweenCondition', - 'IN' => 'buildInCondition', - 'NOT IN' => 'buildInCondition', - 'LIKE' => 'buildLikeCondition', - 'NOT LIKE' => 'buildLikeCondition', - 'OR LIKE' => 'buildLikeCondition', - 'OR NOT LIKE' => 'buildLikeCondition', - ); - - if (!is_array($condition)) { - return (string)$condition; - } elseif ($condition === array()) { - return ''; - } - if (isset($condition[0])) { // operator format: operator, operand 1, operand 2, ... - $operator = strtoupper($condition[0]); - if (isset($builders[$operator])) { - $method = $builders[$operator]; - array_shift($condition); - return $this->$method($operator, $condition); - } else { - throw new Exception('Found unknown operator in query: ' . $operator); - } - } else { // hash format: 'column1'=>'value1', 'column2'=>'value2', ... - return $this->buildHashCondition($condition); - } - } - - private function buildHashCondition($condition) - { - $parts = array(); - foreach ($condition as $column => $value) { - if (is_array($value)) { // IN condition - $parts[] = $this->buildInCondition('in', array($column, $value)); - } else { - if (strpos($column, '(') === false) { - $column = $this->db->quoteColumnName($column); - } - if ($value === null) { - $parts[] = "$column IS NULL"; - } elseif (is_string($value)) { - $parts[] = "$column=" . $this->db->quoteValue($value); - } else { - $parts[] = "$column=$value"; - } - } - } - return count($parts) === 1 ? $parts[0] : '(' . implode(') AND (', $parts) . ')'; - } - - private function buildAndCondition($operator, $operands) - { - $parts = array(); - foreach ($operands as $operand) { - if (is_array($operand)) { - $operand = $this->buildCondition($operand); - } - if ($operand !== '') { - $parts[] = $operand; - } - } - if ($parts !== array()) { - return '(' . implode(") $operator (", $parts) . ')'; - } else { - return ''; - } - } - - private function buildBetweenCondition($operator, $operands) - { - if (!isset($operands[0], $operands[1], $operands[2])) { - throw new Exception("Operator '$operator' requires three operands."); - } - - list($column, $value1, $value2) = $operands; - - if (strpos($column, '(') === false) { - $column = $this->db->quoteColumnName($column); - } - $value1 = is_string($value1) ? $this->db->quoteValue($value1) : (string)$value1; - $value2 = is_string($value2) ? $this->db->quoteValue($value2) : (string)$value2; - - return "$column $operator $value1 AND $value2"; - } - - private function buildInCondition($operator, $operands) - { - if (!isset($operands[0], $operands[1])) { - throw new Exception("Operator '$operator' requires two operands."); - } - - list($column, $values) = $operands; - - $values = (array)$values; - - if ($values === array() || $column === array()) { - return $operator === 'IN' ? '0=1' : ''; - } - - 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 { - $values[$i] = is_string($value) ? $this->db->quoteValue($value) : (string)$value; - } - } - if (strpos($column, '(') === false) { - $column = $this->db->quoteColumnName($column); - } - - if (count($values) > 1) { - return "$column $operator (" . implode(', ', $values) . ')'; - } else { - $operator = $operator === 'IN' ? '=' : '<>'; - return "$column$operator{$values[0]}"; - } - } - - protected function buildCompositeInCondition($operator, $columns, $values) - { - foreach ($columns as $i => $column) { - if (strpos($column, '(') === false) { - $columns[$i] = $this->db->quoteColumnName($column); - } - } - $vss = array(); - foreach ($values as $value) { - $vs = array(); - foreach ($columns as $column) { - if (isset($value[$column])) { - $vs[] = is_string($value[$column]) ? $this->db->quoteValue($value[$column]) : (string)$value[$column]; - } else { - $vs[] = 'NULL'; - } - } - $vss[] = '(' . implode(', ', $vs) . ')'; - } - return '(' . implode(', ', $columns) . ") $operator (" . implode(', ', $vss) . ')'; - } - - private function buildLikeCondition($operator, $operands) - { - if (!isset($operands[0], $operands[1])) { - throw new Exception("Operator '$operator' requires two operands."); - } - - list($column, $values) = $operands; - - $values = (array)$values; - - if ($values === array()) { - return $operator === 'LIKE' || $operator === 'OR LIKE' ? '0=1' : ''; - } - - if ($operator === 'LIKE' || $operator === 'NOT LIKE') { - $andor = ' AND '; - } else { - $andor = ' OR '; - $operator = $operator === 'OR LIKE' ? 'LIKE' : 'NOT LIKE'; - } - - if (strpos($column, '(') === false) { - $column = $this->db->quoteColumnName($column); - } - - $parts = array(); - foreach ($values as $value) { - $parts[] = "$column $operator " . $this->db->quoteValue($value); - } - - return implode($andor, $parts); - } - - /** * @param array $columns * @param boolean $distinct * @param string $selectOption @@ -737,10 +544,11 @@ class QueryBuilder extends \yii\base\Object /** * @param string|array $joins + * @param array $params the binding parameters to be populated * @return string the JOIN clause built from [[query]]. * @throws Exception if the $joins parameter is not in proper format */ - public function buildJoin($joins) + public function buildJoin($joins, &$params) { if (empty($joins)) { return ''; @@ -761,9 +569,9 @@ class QueryBuilder extends \yii\base\Object } $joins[$i] = $join[0] . ' ' . $table; if (isset($join[2])) { - $condition = $this->buildCondition($join[2]); + $condition = $this->buildCondition($join[2], $params); if ($condition !== '') { - $joins[$i] .= ' ON ' . $this->buildCondition($join[2]); + $joins[$i] .= ' ON ' . $condition; } } } else { @@ -776,11 +584,12 @@ class QueryBuilder extends \yii\base\Object /** * @param string|array $condition + * @param array $params the binding parameters to be populated * @return string the WHERE clause built from [[query]]. */ - public function buildWhere($condition) + public function buildWhere($condition, &$params) { - $where = $this->buildCondition($condition); + $where = $this->buildCondition($condition, $params); return $where === '' ? '' : 'WHERE ' . $where; } @@ -795,11 +604,12 @@ class QueryBuilder extends \yii\base\Object /** * @param string|array $condition + * @param array $params the binding parameters to be populated * @return string the HAVING clause built from [[query]]. */ - public function buildHaving($condition) + public function buildHaving($condition, &$params) { - $having = $this->buildCondition($condition); + $having = $this->buildCondition($condition, $params); return $having === '' ? '' : 'HAVING ' . $having; } @@ -843,16 +653,19 @@ class QueryBuilder extends \yii\base\Object /** * @param array $unions + * @param array $params the binding parameters to be populated * @return string the UNION clause built from [[query]]. */ - public function buildUnion($unions) + public function buildUnion($unions, &$params) { if (empty($unions)) { return ''; } foreach ($unions as $i => $union) { if ($union instanceof Query) { + $union->addParams($params); $unions[$i] = $this->build($union); + $params = $union->params; } } return "UNION (\n" . implode("\n) UNION (\n", $unions) . "\n)"; @@ -864,7 +677,7 @@ class QueryBuilder extends \yii\base\Object * @param string|array $columns the columns to be processed * @return string the processing result */ - protected function buildColumns($columns) + public function buildColumns($columns) { if (!is_array($columns)) { if (strpos($columns, '(') !== false) { @@ -882,4 +695,208 @@ class QueryBuilder extends \yii\base\Object } return is_array($columns) ? implode(', ', $columns) : $columns; } + + + /** + * Parses the condition specification and generates the corresponding SQL expression. + * @param string|array $condition the condition specification. Please refer to [[Query::where()]] + * on how to specify a condition. + * @param array $params the binding parameters to be populated + * @return string the generated SQL expression + * @throws \yii\db\Exception if the condition is in bad format + */ + public function buildCondition($condition, &$params) + { + static $builders = array( + 'AND' => 'buildAndCondition', + 'OR' => 'buildAndCondition', + 'BETWEEN' => 'buildBetweenCondition', + 'NOT BETWEEN' => 'buildBetweenCondition', + 'IN' => 'buildInCondition', + 'NOT IN' => 'buildInCondition', + 'LIKE' => 'buildLikeCondition', + 'NOT LIKE' => 'buildLikeCondition', + 'OR LIKE' => 'buildLikeCondition', + 'OR NOT LIKE' => 'buildLikeCondition', + ); + + if (!is_array($condition)) { + return (string)$condition; + } elseif ($condition === array()) { + return ''; + } + if (isset($condition[0])) { // operator format: operator, operand 1, operand 2, ... + $operator = strtoupper($condition[0]); + if (isset($builders[$operator])) { + $method = $builders[$operator]; + array_shift($condition); + return $this->$method($operator, $condition, $params); + } else { + throw new Exception('Found unknown operator in query: ' . $operator); + } + } else { // hash format: 'column1'=>'value1', 'column2'=>'value2', ... + return $this->buildHashCondition($condition, $params); + } + } + + private function buildHashCondition($condition, &$params) + { + $parts = array(); + foreach ($condition as $column => $value) { + if (is_array($value)) { // IN condition + $parts[] = $this->buildInCondition('in', array($column, $value), $query); + } else { + if (strpos($column, '(') === false) { + $column = $this->db->quoteColumnName($column); + } + if ($value === null) { + $parts[] = "$column IS NULL"; + } else { + $phName = self::PARAM_PREFIX . count($params); + $parts[] = "$column=$phName"; + $params[$phName] = $value; + } + } + } + return count($parts) === 1 ? $parts[0] : '(' . implode(') AND (', $parts) . ')'; + } + + private function buildAndCondition($operator, $operands, &$params) + { + $parts = array(); + foreach ($operands as $operand) { + if (is_array($operand)) { + $operand = $this->buildCondition($operand, $params); + } + if ($operand !== '') { + $parts[] = $operand; + } + } + if ($parts !== array()) { + return '(' . implode(") $operator (", $parts) . ')'; + } else { + return ''; + } + } + + private function buildBetweenCondition($operator, $operands, &$params) + { + if (!isset($operands[0], $operands[1], $operands[2])) { + throw new Exception("Operator '$operator' requires three operands."); + } + + list($column, $value1, $value2) = $operands; + + if (strpos($column, '(') === false) { + $column = $this->db->quoteColumnName($column); + } + $phName1 = self::PARAM_PREFIX . count($params); + $phName2 = self::PARAM_PREFIX . count($params); + $params[$phName1] = $value1; + $params[$phName2] = $value2; + + return "$column $operator $phName1 AND $phName2"; + } + + private function buildInCondition($operator, $operands, &$params) + { + if (!isset($operands[0], $operands[1])) { + throw new Exception("Operator '$operator' requires two operands."); + } + + list($column, $values) = $operands; + + $values = (array)$values; + + if ($values === array() || $column === array()) { + return $operator === 'IN' ? '0=1' : ''; + } + + if (count($column) > 1) { + return $this->buildCompositeInCondition($operator, $column, $values, $params); + } 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 { + $phName = self::PARAM_PREFIX . count($params); + $params[$phName] = $value; + $values[$i] = $phName; + } + } + if (strpos($column, '(') === false) { + $column = $this->db->quoteColumnName($column); + } + + if (count($values) > 1) { + return "$column $operator (" . implode(', ', $values) . ')'; + } else { + $operator = $operator === 'IN' ? '=' : '<>'; + return "$column$operator{$values[0]}"; + } + } + + protected function buildCompositeInCondition($operator, $columns, $values, &$params) + { + foreach ($columns as $i => $column) { + if (strpos($column, '(') === false) { + $columns[$i] = $this->db->quoteColumnName($column); + } + } + $vss = array(); + foreach ($values as $value) { + $vs = array(); + foreach ($columns as $column) { + if (isset($value[$column])) { + $phName = self::PARAM_PREFIX . count($params); + $params[$phName] = $value[$column]; + $vs[] = $phName; + } else { + $vs[] = 'NULL'; + } + } + $vss[] = '(' . implode(', ', $vs) . ')'; + } + return '(' . implode(', ', $columns) . ") $operator (" . implode(', ', $vss) . ')'; + } + + private function buildLikeCondition($operator, $operands, &$params) + { + if (!isset($operands[0], $operands[1])) { + throw new Exception("Operator '$operator' requires two operands."); + } + + list($column, $values) = $operands; + + $values = (array)$values; + + if ($values === array()) { + return $operator === 'LIKE' || $operator === 'OR LIKE' ? '0=1' : ''; + } + + if ($operator === 'LIKE' || $operator === 'NOT LIKE') { + $andor = ' AND '; + } else { + $andor = ' OR '; + $operator = $operator === 'OR LIKE' ? 'LIKE' : 'NOT LIKE'; + } + + if (strpos($column, '(') === false) { + $column = $this->db->quoteColumnName($column); + } + + $parts = array(); + foreach ($values as $value) { + $phName = self::PARAM_PREFIX . count($params); + $params[$phName] = $value; + $parts[] = "$column $operator $phName"; + } + + return implode($andor, $parts); + } } From 597082a11aa49b59826b2300fd90122bba1cf947 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Sat, 30 Mar 2013 18:28:54 -0400 Subject: [PATCH 013/104] Automatic table and column name quoting. --- framework/caching/DbCache.php | 6 +++--- framework/db/Command.php | 25 ++-------------------- framework/db/Connection.php | 34 +++++++++++++++++++----------- framework/db/Schema.php | 10 ++++----- framework/logging/DbTarget.php | 3 ++- framework/web/DbSession.php | 4 ++-- tests/unit/framework/db/ConnectionTest.php | 1 - 7 files changed, 36 insertions(+), 47 deletions(-) diff --git a/framework/caching/DbCache.php b/framework/caching/DbCache.php index 3952852..dee8c7a 100644 --- a/framework/caching/DbCache.php +++ b/framework/caching/DbCache.php @@ -99,7 +99,7 @@ class DbCache extends Cache $query = new Query; $query->select(array('data')) ->from($this->cacheTable) - ->where('id = :id AND (expire = 0 OR expire >' . time() . ')', array(':id' => $key)); + ->where('[[id]] = :id AND ([[expire]] = 0 OR [[expire]] >' . time() . ')', array(':id' => $key)); if ($this->db->enableQueryCache) { // temporarily disable and re-enable query caching $this->db->enableQueryCache = false; @@ -125,7 +125,7 @@ class DbCache extends Cache $query->select(array('id', 'data')) ->from($this->cacheTable) ->where(array('id' => $keys)) - ->andWhere('(expire = 0 OR expire > ' . time() . ')'); + ->andWhere('([[expire]] = 0 OR [[expire]] > ' . time() . ')'); if ($this->db->enableQueryCache) { $this->db->enableQueryCache = false; @@ -227,7 +227,7 @@ class DbCache extends Cache { if ($force || mt_rand(0, 1000000) < $this->gcProbability) { $this->db->createCommand() - ->delete($this->cacheTable, 'expire > 0 AND expire < ' . time()) + ->delete($this->cacheTable, '[[expire]] > 0 AND [[expire]] < ' . time()) ->execute(); } } diff --git a/framework/db/Command.php b/framework/db/Command.php index 2a13a89..73a8346 100644 --- a/framework/db/Command.php +++ b/framework/db/Command.php @@ -84,42 +84,21 @@ class Command extends \yii\base\Component /** * Specifies the SQL statement to be executed. - * Any previous execution will be terminated or cancelled. + * The previous SQL execution (if any) will be cancelled, and [[params]] will be cleared as well. * @param string $sql the SQL statement to be set. * @return Command this command instance */ public function setSql($sql) { if ($sql !== $this->_sql) { - if ($this->db->enableAutoQuoting && $sql != '') { - $sql = $this->expandSql($sql); - } $this->cancel(); - $this->_sql = $sql; + $this->_sql = $this->db->quoteSql($sql); $this->_params = array(); } return $this; } /** - * Expands a SQL statement by quoting table and column names and replacing table prefixes. - * @param string $sql the SQL to be expanded - * @return string the expanded SQL - */ - protected function expandSql($sql) - { - $db = $this->db; - return preg_replace_callback('/(\\{\\{(.*?)\\}\\}|\\[\\[(.*?)\\]\\])/', function($matches) use($db) { - if (isset($matches[3])) { - return $db->quoteColumnName($matches[3]); - } else { - $name = str_replace('%', $db->tablePrefix, $matches[2]); - return $db->quoteTableName($name); - } - }, $sql); - } - - /** * Prepares the SQL statement to be executed. * For complex SQL statement that is to be executed multiple times, * this may improve performance. diff --git a/framework/db/Connection.php b/framework/db/Connection.php index 1be43eb..695034a 100644 --- a/framework/db/Connection.php +++ b/framework/db/Connection.php @@ -223,21 +223,10 @@ class Connection extends Component * @var string the common prefix or suffix for table names. If a table name is given * as `{{%TableName}}`, then the percentage character `%` will be replaced with this * property value. For example, `{{%post}}` becomes `{{tbl_post}}` if this property is - * set as `"tbl_"`. Note that this property is only effective when [[enableAutoQuoting]] - * is true. - * @see enableAutoQuoting + * set as `"tbl_"`. */ public $tablePrefix; /** - * @var boolean whether to enable automatic quoting of table names and column names. - * Defaults to true. When this property is true, any token enclosed within double curly brackets - * (e.g. `{{post}}`) in a SQL statement will be treated as a table name and will be quoted - * accordingly when the SQL statement is executed; and any token enclosed within double square - * brackets (e.g. `[[name]]`) will be treated as a column name and quoted accordingly. - * @see tablePrefix - */ - public $enableAutoQuoting = true; - /** * @var array mapping between PDO driver names and [[Schema]] classes. * The keys of the array are PDO driver names while the values the corresponding * schema class name or configuration. Please refer to [[\Yii::createObject()]] for @@ -518,6 +507,27 @@ class Connection extends Component } /** + * Processes a SQL statement by quoting table and column names that are enclosed within double brackets. + * Tokens enclosed within double curly brackets are treated as table names, while + * tokens enclosed within double square brackets are column names. They will be quoted accordingly. + * Also, the percentage character "%" in a table name will be replaced with [[tablePrefix]]. + * @param string $sql the SQL to be quoted + * @return string the quoted SQL + */ + public function quoteSql($sql) + { + $db = $this; + return preg_replace_callback('/(\\{\\{([\w\-\. ]+)\\}\\}|\\[\\[([\w\-\. ]+)\\]\\])/', + function($matches) use($db) { + if (isset($matches[3])) { + return $db->quoteColumnName($matches[3]); + } else { + return str_replace('%', $this->tablePrefix, $db->quoteTableName($matches[2])); + } + }, $sql); + } + + /** * Returns the name of the DB driver for the current [[dsn]]. * @return string name of the DB driver */ diff --git a/framework/db/Schema.php b/framework/db/Schema.php index 71bc9a2..d8d89bc 100644 --- a/framework/db/Schema.php +++ b/framework/db/Schema.php @@ -248,7 +248,7 @@ abstract class Schema extends \yii\base\Object /** * Quotes a table name for use in a query. * If the table name contains schema prefix, the prefix will also be properly quoted. - * If the table name is already quoted or contains special characters including '(', '[[' and '{{', + * If the table name is already quoted or contains '(' or '{{', * then this method will do nothing. * @param string $name table name * @return string the properly quoted table name @@ -256,7 +256,7 @@ abstract class Schema extends \yii\base\Object */ public function quoteTableName($name) { - if (strpos($name, '(') !== false || strpos($name, '[[') !== false || strpos($name, '{{') !== false) { + if (strpos($name, '(') !== false || strpos($name, '{{') !== false) { return $name; } if (strpos($name, '.') === false) { @@ -273,7 +273,7 @@ abstract class Schema extends \yii\base\Object /** * Quotes a column name for use in a query. * If the column name contains prefix, the prefix will also be properly quoted. - * If the column name is already quoted or contains special characters including '(', '[[' and '{{', + * If the column name is already quoted or contains '(', '[[' or '{{', * then this method will do nothing. * @param string $name column name * @return string the properly quoted column name @@ -320,13 +320,13 @@ abstract class Schema extends \yii\base\Object /** * Returns the real name of a table name. * This method will strip off curly brackets from the given table name - * and replace the percentage character in the name with [[Connection::tablePrefix]]. + * and replace the percentage character '%' with [[Connection::tablePrefix]]. * @param string $name the table name to be converted * @return string the real name of the given table name */ public function getRealTableName($name) { - if ($this->db->enableAutoQuoting && strpos($name, '{{') !== false) { + if (strpos($name, '{{') !== false) { $name = preg_replace('/\\{\\{(.*?)\\}\\}/', '\1', $name); return str_replace('%', $this->db->tablePrefix, $name); } else { diff --git a/framework/logging/DbTarget.php b/framework/logging/DbTarget.php index e4e30ce..ce9d843 100644 --- a/framework/logging/DbTarget.php +++ b/framework/logging/DbTarget.php @@ -78,7 +78,8 @@ class DbTarget extends Target public function export($messages) { $tableName = $this->db->quoteTableName($this->logTable); - $sql = "INSERT INTO $tableName (level, category, log_time, message) VALUES (:level, :category, :log_time, :message)"; + $sql = "INSERT INTO $tableName ([[level]], [[category]], [[log_time]], [[message]]) + VALUES (:level, :category, :log_time, :message)"; $command = $this->db->createCommand($sql); foreach ($messages as $message) { $command->bindValues(array( diff --git a/framework/web/DbSession.php b/framework/web/DbSession.php index d3afc76..2910b40 100644 --- a/framework/web/DbSession.php +++ b/framework/web/DbSession.php @@ -144,7 +144,7 @@ class DbSession extends Session $query = new Query; $data = $query->select(array('data')) ->from($this->sessionTable) - ->where('expire>:expire AND id=:id', array(':expire' => time(), ':id' => $id)) + ->where('[[expire]]>:expire AND [[id]]=:id', array(':expire' => time(), ':id' => $id)) ->createCommand($this->db) ->queryScalar(); return $data === false ? '' : $data; @@ -214,7 +214,7 @@ class DbSession extends Session public function gcSession($maxLifetime) { $this->db->createCommand() - ->delete($this->sessionTable, 'expire<:expire', array(':expire' => time())) + ->delete($this->sessionTable, '[[expire]]<:expire', array(':expire' => time())) ->execute(); return true; } diff --git a/tests/unit/framework/db/ConnectionTest.php b/tests/unit/framework/db/ConnectionTest.php index afb4f20..256c5a9 100644 --- a/tests/unit/framework/db/ConnectionTest.php +++ b/tests/unit/framework/db/ConnectionTest.php @@ -59,7 +59,6 @@ class ConnectionTest extends \yiiunit\MysqlTestCase $this->assertEquals('`table`', $connection->quoteTableName('`table`')); $this->assertEquals('`schema`.`table`', $connection->quoteTableName('schema.table')); $this->assertEquals('`schema`.`table`', $connection->quoteTableName('schema.`table`')); - $this->assertEquals('[[table]]', $connection->quoteTableName('[[table]]')); $this->assertEquals('{{table}}', $connection->quoteTableName('{{table}}')); $this->assertEquals('(table)', $connection->quoteTableName('(table)')); } From b0cc86dfcd08176b1df0267531f50a6dcdf874cb Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Sat, 30 Mar 2013 21:39:31 -0400 Subject: [PATCH 014/104] added Command::getRawSql() --- framework/db/Command.php | 57 ++++++++++++++++++++++++++++++++++-------------- framework/db/Schema.php | 6 ++--- 2 files changed, 44 insertions(+), 19 deletions(-) diff --git a/framework/db/Command.php b/framework/db/Command.php index 73a8346..dc6c972 100644 --- a/framework/db/Command.php +++ b/framework/db/Command.php @@ -99,6 +99,39 @@ class Command extends \yii\base\Component } /** + * Returns the raw SQL by inserting parameter values into the corresponding placeholders in [[sql]]. + * Note that the return value of this method should mainly be used for logging purpose. + * It is likely that this method returns an invalid SQL due to improper replacement of parameter placeholders. + * @return string the raw SQL + */ + public function getRawSql() + { + if ($this->_params === array()) { + return $this->_sql; + } else { + $params = array(); + foreach ($this->_params as $name => $value) { + if (is_string($value)) { + $params[$name] = $this->db->quoteValue($value); + } elseif ($value === null) { + $params[$name] = 'NULL'; + } else { + $params[$name] = $value; + } + } + if (isset($params[1])) { + $sql = ''; + foreach (explode('?', $this->_sql) as $i => $part) { + $sql .= (isset($params[$i]) ? $params[$i] : '') . $part; + } + return $sql; + } else { + return strtr($this->_sql, $params); + } + } + } + + /** * Prepares the SQL statement to be executed. * For complex SQL statement that is to be executed multiple times, * this may improve performance. @@ -222,6 +255,7 @@ class Command extends \yii\base\Component 'boolean' => \PDO::PARAM_BOOL, 'integer' => \PDO::PARAM_INT, 'string' => \PDO::PARAM_STR, + 'resource' => \PDO::PARAM_LOB, 'NULL' => \PDO::PARAM_NULL, ); $type = gettype($data); @@ -239,13 +273,9 @@ class Command extends \yii\base\Component { $sql = $this->getSql(); - if ($this->_params === array()) { - $paramLog = ''; - } else { - $paramLog = "\nParameters: " . var_export($this->_params, true); - } + $rawSql = $this->getRawSql(); - Yii::trace("Executing SQL: {$sql}{$paramLog}", __METHOD__); + Yii::trace("Executing SQL: $rawSql", __METHOD__); if ($sql == '') { return 0; @@ -271,7 +301,7 @@ class Command extends \yii\base\Component } $message = $e->getMessage(); - Yii::error("$message\nFailed to execute SQL: {$sql}{$paramLog}", __METHOD__); + Yii::error("$message\nFailed to execute SQL: $rawSql", __METHOD__); $errorInfo = $e instanceof \PDOException ? $e->errorInfo : null; throw new Exception($message, $errorInfo, (int)$e->getCode()); @@ -357,13 +387,9 @@ class Command extends \yii\base\Component { $db = $this->db; $sql = $this->getSql(); - if ($this->_params === array()) { - $paramLog = ''; - } else { - $paramLog = "\nParameters: " . var_export($this->_params, true); - } + $rawSql = $this->getRawSql(); - Yii::trace("Querying SQL: {$sql}{$paramLog}", __METHOD__); + Yii::trace("Querying SQL: $rawSql", __METHOD__); /** @var $cache \yii\caching\Cache */ if ($db->enableQueryCache && $method !== '') { @@ -375,8 +401,7 @@ class Command extends \yii\base\Component __CLASS__, $db->dsn, $db->username, - $sql, - $paramLog, + $rawSql, )); if (($result = $cache->get($cacheKey)) !== false) { Yii::trace('Query result served from cache', __METHOD__); @@ -418,7 +443,7 @@ class Command extends \yii\base\Component Yii::endProfile($token, __METHOD__); } $message = $e->getMessage(); - Yii::error("$message\nCommand::$method() failed: {$sql}{$paramLog}", __METHOD__); + Yii::error("$message\nCommand::$method() failed: $rawSql", __METHOD__); $errorInfo = $e instanceof \PDOException ? $e->errorInfo : null; throw new Exception($message, $errorInfo, (int)$e->getCode()); } diff --git a/framework/db/Schema.php b/framework/db/Schema.php index d8d89bc..9538e4c 100644 --- a/framework/db/Schema.php +++ b/framework/db/Schema.php @@ -83,7 +83,7 @@ abstract class Schema extends \yii\base\Object } $db = $this->db; - $realName = $this->getRealTableName($name); + $realName = $this->getRawTableName($name); if ($db->enableSchemaCache && !in_array($name, $db->schemaCacheExclude, true)) { /** @var $cache Cache */ @@ -318,13 +318,13 @@ abstract class Schema extends \yii\base\Object } /** - * Returns the real name of a table name. + * Returns the actual name of a given table name. * This method will strip off curly brackets from the given table name * and replace the percentage character '%' with [[Connection::tablePrefix]]. * @param string $name the table name to be converted * @return string the real name of the given table name */ - public function getRealTableName($name) + public function getRawTableName($name) { if (strpos($name, '{{') !== false) { $name = preg_replace('/\\{\\{(.*?)\\}\\}/', '\1', $name); From c32a202d824eca55dfd86aea3b575c081bcad0cb Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Sun, 31 Mar 2013 20:21:29 -0400 Subject: [PATCH 015/104] refactored MVC. --- framework/base/Controller.php | 119 +++++++++++++++++++++++++-------- framework/base/View.php | 93 +++++++++----------------- framework/base/Widget.php | 57 +++++++++++----- framework/web/PageCache.php | 5 -- framework/widgets/Clip.php | 8 +-- framework/widgets/ContentDecorator.php | 21 ++---- framework/widgets/FragmentCache.php | 9 --- 7 files changed, 174 insertions(+), 138 deletions(-) diff --git a/framework/base/Controller.php b/framework/base/Controller.php index ff6d8f7..241c66c 100644 --- a/framework/base/Controller.php +++ b/framework/base/Controller.php @@ -47,6 +47,11 @@ class Controller extends Component * by [[run()]] when it is called by [[Application]] to run an action. */ public $action; + /** + * @var View the view object that can be used to render views or view files. + */ + private $_view; + /** * @param string $id the ID of this controller @@ -135,7 +140,7 @@ class Controller extends Component } elseif ($pos > 0) { return $this->module->runAction($route, $params); } else { - return \Yii::$app->runAction(ltrim($route, '/'), $params); + return Yii::$app->runAction(ltrim($route, '/'), $params); } } @@ -293,6 +298,37 @@ class Controller extends Component /** * Renders a view and applies layout if available. + * + * The view to be rendered 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 [[module]]. + * - relative path (e.g. "index"): the actual view file will be looked for under [[viewPath]]. + * + * To determine which layout should be applied, the following two steps are conducted: + * + * 1. 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. + * + * 2. 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`. + * * @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. @@ -301,10 +337,11 @@ class Controller extends Component */ public function render($view, $params = array()) { - $output = Yii::$app->getView()->render($view, $params, $this); + $viewFile = $this->findViewFile($view); + $output = $this->getView()->renderFile($viewFile, $params, $this); $layoutFile = $this->findLayoutFile(); if ($layoutFile !== false) { - return Yii::$app->getView()->renderFile($layoutFile, array('content' => $output), $this); + return $this->getView()->renderFile($layoutFile, array('content' => $output), $this); } else { return $output; } @@ -313,14 +350,14 @@ class Controller extends Component /** * 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 string $view the view name. Please refer to [[render()]] 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 Yii::$app->getView()->render($view, $params, $this); + return $this->getView()->render($view, $params, $this); } /** @@ -332,7 +369,30 @@ class Controller extends Component */ public function renderFile($file, $params = array()) { - return Yii::$app->getView()->renderFile($file, $params, $this); + return $this->getView()->renderFile($file, $params, $this); + } + + /** + * Returns the view object that can be used to render views or view files. + * The [[render()]], [[renderPartial()]] and [[renderFile()]] methods will use + * this view object to implement the actual view rendering. + * @return View the view object that can be used to render views or view files. + */ + public function getView() + { + if ($this->_view === null) { + $this->_view = Yii::$app->getView(); + } + return $this->_view; + } + + /** + * Sets the view object to be used by this controller. + * @param View $view the view object that can be used to render views or view files. + */ + public function setView($view) + { + $this->_view = $view; } /** @@ -347,30 +407,33 @@ class Controller extends Component } /** + * Finds the view file based on the given view name. + * @param string $view the view name or the path alias of the view file. Please refer to [[render()]] + * on how to specify this parameter. + * @return string the view file path. Note that the file may not exist. + */ + protected function findViewFile($view) + { + if (strncmp($view, '@', 1) === 0) { + // e.g. "@app/views/main" + $file = Yii::getAlias($view); + } elseif (strncmp($view, '//', 2) === 0) { + // e.g. "//layouts/main" + $file = Yii::$app->getViewPath() . DIRECTORY_SEPARATOR . ltrim($view, '/'); + } elseif (strncmp($view, '/', 1) === 0) { + // e.g. "/site/index" + $file = $this->module->getViewPath() . DIRECTORY_SEPARATOR . ltrim($view, '/'); + } else { + $file = $this->getViewPath() . DIRECTORY_SEPARATOR . $view; + } + + return FileHelper::getExtension($file) === '' ? $file . '.php' : $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. + * Please refer to [[render()]] on how to specify this parameter. * @throws InvalidParamException if an invalid path alias is used to specify the layout */ protected function findLayoutFile() diff --git a/framework/base/View.php b/framework/base/View.php index c7087c1..d3d9339 100644 --- a/framework/base/View.php +++ b/framework/base/View.php @@ -79,22 +79,29 @@ class View extends Component /** * Renders a view. * - * 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. + * This method delegates the call to the [[context]] object: * - * @param string $view the view name. Please refer to [[findViewFile()]] on how to specify this parameter. + * - If [[context]] is a controller, the [[Controller::renderPartial()]] method will be called; + * - If [[context]] is a widget, the [[Widget::render()]] method will be called; + * - Otherwise, an InvalidCallException exception will be thrown. + * + * @param string $view the view name. Please refer to [[Controller::findViewFile()]] + * and [[Widget::findViewFile()]] on how to specify this parameter. * @param array $params the parameters (name-value pairs) that will be extracted and made available in the view file. - * @param object $context the context that the view should use for rendering the view. If null, - * existing [[context]] will be used. * @return string the rendering result + * @throws InvalidCallException if [[context]] is neither a controller nor a widget. * @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(), $context = null) + public function render($view, $params = array()) { - $viewFile = $this->findViewFile($context, $view); - return $this->renderFile($viewFile, $params, $context); + if ($this->context instanceof Controller) { + return $this->context->renderPartial($view, $params); + } elseif ($this->context instanceof Widget) { + return $this->context->render($view, $params); + } else { + throw new InvalidCallException('View::render() is not supported for the current context.'); + } } /** @@ -213,49 +220,6 @@ 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 [[Controller::viewPath|viewPath]] - * of the context object, assuming the context is either a [[Controller]] or a [[Widget]]. - * - * If the view name does not contain a file extension, it will use the default one `.php`. - * - * @param object $context the view context object - * @param string $view the view name or the path alias of the view file. - * @return string the view file path. Note that the file may not exist. - * @throws InvalidParamException if the view file is an invalid path alias or the context cannot be - * used to determine the actual view file corresponding to the specified view. - */ - protected function findViewFile($context, $view) - { - if (strncmp($view, '@', 1) === 0) { - // e.g. "@app/views/main" - $file = Yii::getAlias($view); - } elseif (strncmp($view, '//', 2) === 0) { - // e.g. "//layouts/main" - $file = Yii::$app->getViewPath() . DIRECTORY_SEPARATOR . ltrim($view, '/'); - } elseif (strncmp($view, '/', 1) === 0) { - // e.g. "/site/index" - $file = Yii::$app->controller->module->getViewPath() . DIRECTORY_SEPARATOR . ltrim($view, '/'); - } elseif ($context instanceof Controller || $context instanceof Widget) { - /** @var $context Controller|Widget */ - $file = $context->getViewPath() . DIRECTORY_SEPARATOR . $view; - } else { - throw new InvalidParamException("Unable to resolve the view file for '$view'."); - } - - return FileHelper::getExtension($file) === '' ? $file . '.php' : $file; - } - - /** * Creates a widget. * This method will use [[Yii::createObject()]] to create the widget. * @param string $class the widget class name or path alias @@ -265,7 +229,10 @@ class View extends Component public function createWidget($class, $properties = array()) { $properties['class'] = $class; - return Yii::createObject($properties, $this->context); + if (!isset($properties['view'])) { + $properties['view'] = $this; + } + return Yii::createObject($properties, $this); } /** @@ -341,7 +308,6 @@ class View extends Component return $this->beginWidget('yii\widgets\Clip', array( 'id' => $id, 'renderInPlace' => $renderInPlace, - 'view' => $this, )); } @@ -355,17 +321,25 @@ class View extends Component /** * 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. + * This method can be used to implement nested layout. For example, a layout can be embedded + * in another layout file specified as '@app/view/layouts/base' like the following: + * + * ~~~ + * beginContent('@app/view/layouts/base'); ?> + * ...layout content here... + * endContent(); ?> + * ~~~ + * + * @param string $viewFile the view file that will be used to decorate the content enclosed by this widget. + * This can be specified as either the view file path or path alias. * @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()) + public function beginContent($viewFile, $params = array()) { return $this->beginWidget('yii\widgets\ContentDecorator', array( - 'view' => $this, - 'viewName' => $view, + 'viewFile' => $viewFile, 'params' => $params, )); } @@ -400,7 +374,6 @@ 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\FragmentCache', $properties); if ($cache->getCachedContent() !== false) { diff --git a/framework/base/Widget.php b/framework/base/Widget.php index 24d0685..c6667fa 100644 --- a/framework/base/Widget.php +++ b/framework/base/Widget.php @@ -19,9 +19,11 @@ use yii\helpers\FileHelper; class Widget extends Component { /** - * @var Widget|Controller the owner/creator of this widget. It could be either a widget or a controller. + * @var View the view object that is used to create this widget. + * This property is automatically set by [[View::createWidget()]]. + * This property is required by [[render()]] and [[renderFile()]]. */ - public $owner; + public $view; /** * @var string id of the widget. */ @@ -32,17 +34,6 @@ class Widget extends Component private static $_counter = 0; /** - * Constructor. - * @param Widget|Controller $owner owner/creator of this 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); - } - - /** * Returns the ID of the widget. * @param boolean $autoGenerate whether to generate an ID if it is not set previously * @return string ID of the widget. @@ -73,6 +64,18 @@ class Widget extends Component /** * Renders a view. + * The view to be rendered 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. 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. @@ -80,7 +83,7 @@ class Widget extends Component */ public function render($view, $params = array()) { - return Yii::$app->getView()->render($view, $params, $this); + return $this->view->render($view, $params, $this); } /** @@ -92,7 +95,7 @@ class Widget extends Component */ public function renderFile($file, $params = array()) { - return Yii::$app->getView()->renderFile($file, $params, $this); + return $this->view->renderFile($file, $params, $this); } /** @@ -106,4 +109,28 @@ 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. + * @param string $view the view name or the path alias of the view file. Please refer to [[render()]] + * on how to specify this parameter. + * @return string the view file path. Note that the file may not exist. + */ + protected function findViewFile($view) + { + if (strncmp($view, '@', 1) === 0) { + // e.g. "@app/views/main" + $file = Yii::getAlias($view); + } elseif (strncmp($view, '//', 2) === 0) { + // e.g. "//layouts/main" + $file = Yii::$app->getViewPath() . DIRECTORY_SEPARATOR . ltrim($view, '/'); + } elseif (strncmp($view, '/', 1) === 0 && Yii::$app->controller !== null) { + // e.g. "/site/index" + $file = Yii::$app->controller->module->getViewPath() . DIRECTORY_SEPARATOR . ltrim($view, '/'); + } else { + $file = $this->getViewPath() . DIRECTORY_SEPARATOR . ltrim($view, '/'); + } + + return FileHelper::getExtension($file) === '' ? $file . '.php' : $file; + } } \ No newline at end of file diff --git a/framework/web/PageCache.php b/framework/web/PageCache.php index 29c8cc8..5a50825 100644 --- a/framework/web/PageCache.php +++ b/framework/web/PageCache.php @@ -25,11 +25,6 @@ class PageCache extends ActionFilter */ public $varyByRoute = true; /** - * @var View the view object that is used to create the fragment cache widget to implement page caching. - * If not set, the view registered with the application will be used. - */ - public $view; - /** * @var string the application component ID of the [[\yii\caching\Cache|cache]] object. */ public $cache = 'cache'; diff --git a/framework/widgets/Clip.php b/framework/widgets/Clip.php index d540b24..f321209 100644 --- a/framework/widgets/Clip.php +++ b/framework/widgets/Clip.php @@ -22,11 +22,6 @@ class Clip extends Widget */ 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. */ @@ -51,7 +46,6 @@ class Clip extends Widget if ($this->renderClip) { echo $clip; } - $view = $this->view !== null ? $this->view : Yii::$app->getView(); - $view->clips[$this->id] = $clip; + $this->view->clips[$this->id] = $clip; } } \ No newline at end of file diff --git a/framework/widgets/ContentDecorator.php b/framework/widgets/ContentDecorator.php index 4c3ae70..3f63621 100644 --- a/framework/widgets/ContentDecorator.php +++ b/framework/widgets/ContentDecorator.php @@ -7,10 +7,8 @@ namespace yii\widgets; -use Yii; use yii\base\InvalidConfigException; use yii\base\Widget; -use yii\base\View; /** * @author Qiang Xue @@ -19,15 +17,10 @@ use yii\base\View; class ContentDecorator extends Widget { /** - * @var View the view object for rendering [[viewName]]. If not set, the view registered with the application - * will be used. + * @var string the view file that will be used to decorate the content enclosed by this widget. + * This can be specified as either the view file path or path alias. */ - public $view; - /** - * @var string the name of the view that will be used to decorate the content enclosed by this widget. - * Please refer to [[View::findViewFile()]] on how to set this property. - */ - public $viewName; + public $viewFile; /** * @var array the parameters (name=>value) to be extracted and made available in the decorative view. */ @@ -38,8 +31,8 @@ class ContentDecorator extends Widget */ public function init() { - if ($this->viewName === null) { - throw new InvalidConfigException('ContentDecorator::viewName must be set.'); + if ($this->viewFile === null) { + throw new InvalidConfigException('ContentDecorator::viewFile must be set.'); } ob_start(); ob_implicit_flush(false); @@ -53,7 +46,7 @@ class ContentDecorator extends Widget { $params = $this->params; $params['content'] = ob_get_clean(); - $view = $this->view !== null ? $this->view : Yii::$app->getView(); - echo $view->render($this->viewName, $params); + // render under the existing context + echo $this->view->renderFile($this->viewFile, $params); } } diff --git a/framework/widgets/FragmentCache.php b/framework/widgets/FragmentCache.php index 65bb86b..dae538a 100644 --- a/framework/widgets/FragmentCache.php +++ b/framework/widgets/FragmentCache.php @@ -64,11 +64,6 @@ class FragmentCache extends Widget */ public $enabled = true; /** - * @var \yii\base\View the view object within which this widget is used. If not set, - * the view registered with the application will be used. This is mainly used by dynamic content feature. - */ - public $view; - /** * @var array a list of placeholders for embedding dynamic contents. This property * is used internally to implement the content caching feature. Do not modify it. */ @@ -81,10 +76,6 @@ class FragmentCache extends Widget { parent::init(); - if ($this->view === null) { - $this->view = Yii::$app->getView(); - } - if (!$this->enabled) { $this->cache = null; } elseif (is_string($this->cache)) { From 906e402ba8823178d1ef7dd9435a030e80c2b488 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Sun, 31 Mar 2013 21:20:42 -0400 Subject: [PATCH 016/104] ActiveRecord::update() and ActiveRecord::delete() now returns the number rows affected. --- framework/db/ActiveRecord.php | 38 +++++++++++++++++++++++++++++--------- framework/db/QueryBuilder.php | 10 ++++++++++ 2 files changed, 39 insertions(+), 9 deletions(-) diff --git a/framework/db/ActiveRecord.php b/framework/db/ActiveRecord.php index d8f2f65..8fd1db2 100644 --- a/framework/db/ActiveRecord.php +++ b/framework/db/ActiveRecord.php @@ -530,8 +530,8 @@ class ActiveRecord extends Model */ public function isAttributeChanged($name) { - if (isset($this->_attribute[$name], $this->_oldAttributes[$name])) { - return $this->_attribute[$name] !== $this->_oldAttributes[$name]; + if (isset($this->_attributes[$name], $this->_oldAttributes[$name])) { + return $this->_attributes[$name] !== $this->_oldAttributes[$name]; } else { return isset($this->_attributes[$name]) || isset($this->_oldAttributes); } @@ -590,7 +590,11 @@ class ActiveRecord extends Model */ public function save($runValidation = true, $attributes = null) { - return $this->getIsNewRecord() ? $this->insert($runValidation, $attributes) : $this->update($runValidation, $attributes); + if ($this->getIsNewRecord()) { + return $this->insert($runValidation, $attributes); + } else { + return $this->update($runValidation, $attributes) !== false; + } } /** @@ -692,11 +696,24 @@ class ActiveRecord extends Model * $customer->update(); * ~~~ * + * Note that it is possible the update does not affect any row in the table. + * In this case, this method will return 0. For this reason, you should use the following + * code to check if update() is successful or not: + * + * ~~~ + * if ($this->update() !== false) { + * // update successful + * } else { + * // update failed + * } + * ~~~ + * * @param boolean $runValidation whether to perform validation before saving the record. * If the validation fails, the record will not be inserted into the database. * @param array $attributes list of attributes that need to be saved. Defaults to null, * meaning all attributes that are loaded from DB will be saved. - * @return boolean whether the attributes are valid and the record is updated successfully. + * @return integer|boolean the number of rows affected, or false if validation fails + * or [[beforeSave()]] stops the updating process. */ public function update($runValidation = true, $attributes = null) { @@ -708,13 +725,15 @@ class ActiveRecord extends Model if ($values !== array()) { // We do not check the return value of updateAll() because it's possible // that the UPDATE statement doesn't change anything and thus returns 0. - $this->updateAll($values, $this->getOldPrimaryKey(true)); + $rows = $this->updateAll($values, $this->getOldPrimaryKey(true)); foreach ($values as $name => $value) { $this->_oldAttributes[$name] = $this->_attributes[$name]; } $this->afterSave(false); + return $rows; + } else { + return 0; } - return true; } else { return false; } @@ -763,17 +782,18 @@ class ActiveRecord extends Model * In the above step 1 and 3, events named [[EVENT_BEFORE_DELETE]] and [[EVENT_AFTER_DELETE]] * will be raised by the corresponding methods. * - * @return boolean whether the deletion is successful. + * @return integer|boolean the number of rows deleted, or false if the deletion is unsuccessful for some reason. + * Note that it is possible the number of rows deleted is 0, even though the deletion execution is successful. */ public function delete() { if ($this->beforeDelete()) { // we do not check the return value of deleteAll() because it's possible // the record is already deleted in the database and thus the method will return 0 - $this->deleteAll($this->getPrimaryKey(true)); + $rows = $this->deleteAll($this->getPrimaryKey(true)); $this->_oldAttributes = null; $this->afterDelete(); - return true; + return $rows; } else { return false; } diff --git a/framework/db/QueryBuilder.php b/framework/db/QueryBuilder.php index 62ef58f..da43940 100644 --- a/framework/db/QueryBuilder.php +++ b/framework/db/QueryBuilder.php @@ -751,6 +751,11 @@ class QueryBuilder extends \yii\base\Object } if ($value === null) { $parts[] = "$column IS NULL"; + } elseif ($value instanceof Expression) { + $parts[] = "$column=" . $value->expression; + foreach ($value->params as $n => $v) { + $params[$n] = $v; + } } else { $phName = self::PARAM_PREFIX . count($params); $parts[] = "$column=$phName"; @@ -823,6 +828,11 @@ class QueryBuilder extends \yii\base\Object } if ($value === null) { $values[$i] = 'NULL'; + } elseif ($value instanceof Expression) { + $values[$i] = $value->expression; + foreach ($value->params as $n => $v) { + $params[$n] = $v; + } } else { $phName = self::PARAM_PREFIX . count($params); $params[$phName] = $value; From 7de94d7fe904016280a8ef831e645e7ac2051a9f Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Sun, 31 Mar 2013 22:03:51 -0400 Subject: [PATCH 017/104] Implemented optimistic locking for AR. --- framework/db/ActiveRecord.php | 63 +++++++++++++++++++++++++++++++---- framework/db/StaleObjectException.php | 23 +++++++++++++ 2 files changed, 79 insertions(+), 7 deletions(-) create mode 100644 framework/db/StaleObjectException.php diff --git a/framework/db/ActiveRecord.php b/framework/db/ActiveRecord.php index 8fd1db2..0de0fb0 100644 --- a/framework/db/ActiveRecord.php +++ b/framework/db/ActiveRecord.php @@ -191,15 +191,12 @@ class ActiveRecord extends Model */ public static function updateAllCounters($counters, $condition = '', $params = array()) { - $db = static::getDb(); $n = 0; foreach ($counters as $name => $value) { - $quotedName = $db->quoteColumnName($name); - $counters[$name] = new Expression("$quotedName+:bp{$n}"); - $params[":bp{$n}"] = $value; + $counters[$name] = new Expression("[[$name]]+:bp{$n}", array(":bp{$n}" => $value)); $n++; } - $command = $db->createCommand(); + $command = static::getDb()->createCommand(); $command->update(static::tableName(), $counters, $condition, $params); return $command->execute(); } @@ -280,6 +277,34 @@ class ActiveRecord extends Model } /** + * Returns the column name that stores the lock version of a table row. + * + * This is used to implement optimistic locking. Optimistic locking allows multiple users + * to access the same record for edits. In case when a user attempts to save the record upon + * some staled data (because another user has modified the data), a [[StaleObjectException]] + * will be thrown, and the update is ignored. + * + * Optimized locking is only supported by [[update()]] and [[delete()]]. + * + * To use optimized locking: + * + * 1. create a column to store the lock version. The column type should be integer (or bigint) + * and default to 0. Override this method to return the name of this column. + * 2. In the Web form that collects the user input, add a hidden field that stores + * the lock version of the recording being updated. + * 3. In the controller action that does the data updating, try to catch the [[StaleObjectException]] + * and implement necessary business logic (e.g. merging the changes, prompting stated data) + * to resolve the conflict. + * + * @return string the column name that stores the lock version of a table row. + * If null is returned (default implemented), optimistic locking will not be supported. + */ + public static function lockVersion() + { + return null; + } + + /** * PHP getter magic method. * This method is overridden so that attributes and related objects can be accessed like properties. * @param string $name property name @@ -714,6 +739,8 @@ class ActiveRecord extends Model * meaning all attributes that are loaded from DB will be saved. * @return integer|boolean the number of rows affected, or false if validation fails * or [[beforeSave()]] stops the updating process. + * @throws StaleObjectException if [[lockVersion|optimistic locking]] is enabled and the data + * being updated is outdated. */ public function update($runValidation = true, $attributes = null) { @@ -723,12 +750,24 @@ class ActiveRecord extends Model if ($this->beforeSave(false)) { $values = $this->getDirtyAttributes($attributes); if ($values !== array()) { + $condition = $this->getOldPrimaryKey(true); + $lock = $this->lockVersion(); + if ($lock !== null) { + $values[$lock] = $this->$lock + 1; + $condition[$lock] = new Expression("[[$lock]]+1"); + } // We do not check the return value of updateAll() because it's possible // that the UPDATE statement doesn't change anything and thus returns 0. - $rows = $this->updateAll($values, $this->getOldPrimaryKey(true)); + $rows = $this->updateAll($values, $condition); + + if ($lock !== null && !$rows) { + throw new StaleObjectException('The object being updated is outdated.'); + } + foreach ($values as $name => $value) { $this->_oldAttributes[$name] = $this->_attributes[$name]; } + $this->afterSave(false); return $rows; } else { @@ -784,13 +823,23 @@ class ActiveRecord extends Model * * @return integer|boolean the number of rows deleted, or false if the deletion is unsuccessful for some reason. * Note that it is possible the number of rows deleted is 0, even though the deletion execution is successful. + * @throws StaleObjectException if [[lockVersion|optimistic locking]] is enabled and the data + * being deleted is outdated. */ public function delete() { if ($this->beforeDelete()) { // we do not check the return value of deleteAll() because it's possible // the record is already deleted in the database and thus the method will return 0 - $rows = $this->deleteAll($this->getPrimaryKey(true)); + $condition = $this->getOldPrimaryKey(true); + $lock = $this->lockVersion(); + if ($lock !== null) { + $condition[$lock] = $this->$lock; + } + $rows = $this->deleteAll($condition); + if ($lock !== null && !$rows) { + throw new StaleObjectException('The object being deleted is outdated.'); + } $this->_oldAttributes = null; $this->afterDelete(); return $rows; diff --git a/framework/db/StaleObjectException.php b/framework/db/StaleObjectException.php new file mode 100644 index 0000000..860c9fc --- /dev/null +++ b/framework/db/StaleObjectException.php @@ -0,0 +1,23 @@ + + * @since 2.0 + */ +class StaleObjectException extends Exception +{ + /** + * @return string the user-friendly name of this exception + */ + public function getName() + { + return \Yii::t('yii|Stale Object Exception'); + } +} \ No newline at end of file From c6a13278971c78238aec831ea9e362b6c8aa126d Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Sun, 31 Mar 2013 22:09:13 -0400 Subject: [PATCH 018/104] renamed lockVersion to optimisticLock --- framework/db/ActiveRecord.php | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/framework/db/ActiveRecord.php b/framework/db/ActiveRecord.php index 0de0fb0..82a3b10 100644 --- a/framework/db/ActiveRecord.php +++ b/framework/db/ActiveRecord.php @@ -277,19 +277,19 @@ class ActiveRecord extends Model } /** - * Returns the column name that stores the lock version of a table row. + * Returns the name of the column that stores the lock version for implementing optimistic locking. * - * This is used to implement optimistic locking. Optimistic locking allows multiple users - * to access the same record for edits. In case when a user attempts to save the record upon - * some staled data (because another user has modified the data), a [[StaleObjectException]] - * will be thrown, and the update is ignored. + * Optimistic locking allows multiple users to access the same record for edits. In case + * when a user attempts to save the record upon some staled data (because another user + * has modified the data), a [[StaleObjectException]] exception will be thrown, and + * the update or deletion is ignored. * * Optimized locking is only supported by [[update()]] and [[delete()]]. * * To use optimized locking: * - * 1. create a column to store the lock version. The column type should be integer (or bigint) - * and default to 0. Override this method to return the name of this column. + * 1. create a column to store the lock version. The column type should be `BIGINT DEFAULT 0`. + * Override this method to return the name of this column. * 2. In the Web form that collects the user input, add a hidden field that stores * the lock version of the recording being updated. * 3. In the controller action that does the data updating, try to catch the [[StaleObjectException]] @@ -299,7 +299,7 @@ class ActiveRecord extends Model * @return string the column name that stores the lock version of a table row. * If null is returned (default implemented), optimistic locking will not be supported. */ - public static function lockVersion() + public function optimisticLock() { return null; } @@ -739,7 +739,7 @@ class ActiveRecord extends Model * meaning all attributes that are loaded from DB will be saved. * @return integer|boolean the number of rows affected, or false if validation fails * or [[beforeSave()]] stops the updating process. - * @throws StaleObjectException if [[lockVersion|optimistic locking]] is enabled and the data + * @throws StaleObjectException if [[optimisticLock|optimistic locking]] is enabled and the data * being updated is outdated. */ public function update($runValidation = true, $attributes = null) @@ -751,7 +751,7 @@ class ActiveRecord extends Model $values = $this->getDirtyAttributes($attributes); if ($values !== array()) { $condition = $this->getOldPrimaryKey(true); - $lock = $this->lockVersion(); + $lock = $this->optimisticLock(); if ($lock !== null) { $values[$lock] = $this->$lock + 1; $condition[$lock] = new Expression("[[$lock]]+1"); @@ -823,7 +823,7 @@ class ActiveRecord extends Model * * @return integer|boolean the number of rows deleted, or false if the deletion is unsuccessful for some reason. * Note that it is possible the number of rows deleted is 0, even though the deletion execution is successful. - * @throws StaleObjectException if [[lockVersion|optimistic locking]] is enabled and the data + * @throws StaleObjectException if [[optimisticLock|optimistic locking]] is enabled and the data * being deleted is outdated. */ public function delete() @@ -832,7 +832,7 @@ class ActiveRecord extends Model // we do not check the return value of deleteAll() because it's possible // the record is already deleted in the database and thus the method will return 0 $condition = $this->getOldPrimaryKey(true); - $lock = $this->lockVersion(); + $lock = $this->optimisticLock(); if ($lock !== null) { $condition[$lock] = $this->$lock; } From 2026d3824bfc48caacb34d225b2046a2f7aab5a5 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Mon, 1 Apr 2013 08:32:52 -0400 Subject: [PATCH 019/104] allow using existing column to store lock version. --- framework/db/ActiveRecord.php | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/framework/db/ActiveRecord.php b/framework/db/ActiveRecord.php index 82a3b10..45c53fb 100644 --- a/framework/db/ActiveRecord.php +++ b/framework/db/ActiveRecord.php @@ -279,16 +279,16 @@ class ActiveRecord extends Model /** * Returns the name of the column that stores the lock version for implementing optimistic locking. * - * Optimistic locking allows multiple users to access the same record for edits. In case - * when a user attempts to save the record upon some staled data (because another user - * has modified the data), a [[StaleObjectException]] exception will be thrown, and - * the update or deletion is ignored. + * Optimistic locking allows multiple users to access the same record for edits and avoids + * potential conflicts. In case when a user attempts to save the record upon some staled data + * (because another user has modified the data), a [[StaleObjectException]] exception will be thrown, + * and the update or deletion is skipped. * * Optimized locking is only supported by [[update()]] and [[delete()]]. * * To use optimized locking: * - * 1. create a column to store the lock version. The column type should be `BIGINT DEFAULT 0`. + * 1. Create a column to store the version number of each row. The column type should be `BIGINT DEFAULT 0`. * Override this method to return the name of this column. * 2. In the Web form that collects the user input, add a hidden field that stores * the lock version of the recording being updated. @@ -753,8 +753,10 @@ class ActiveRecord extends Model $condition = $this->getOldPrimaryKey(true); $lock = $this->optimisticLock(); if ($lock !== null) { - $values[$lock] = $this->$lock + 1; - $condition[$lock] = new Expression("[[$lock]]+1"); + if (!isset($values[$lock])) { + $values[$lock] = $this->$lock + 1; + } + $condition[$lock] = $this->$lock; } // We do not check the return value of updateAll() because it's possible // that the UPDATE statement doesn't change anything and thus returns 0. From 78457bf0091ca7119735516871a6c399499f6fba Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Mon, 1 Apr 2013 14:58:44 -0400 Subject: [PATCH 020/104] fetching plural rule data. --- build/.htaccess | 1 + build/build | 20 + build/build.bat | 23 + build/build.xml | 276 ++++++++++ build/controllers/LocaleController.php | 103 ++++ framework/YiiBase.php | 1 + framework/base/Module.php | 6 + framework/console/Controller.php | 1 - framework/console/controllers/HelpController.php | 4 +- framework/i18n/data/plurals.php | 627 +++++++++++++++++++++++ framework/i18n/data/plurals.xml | 109 ++++ 11 files changed, 1168 insertions(+), 3 deletions(-) create mode 100644 build/.htaccess create mode 100644 build/build create mode 100644 build/build.bat create mode 100644 build/build.xml create mode 100644 build/controllers/LocaleController.php create mode 100644 framework/i18n/data/plurals.php create mode 100644 framework/i18n/data/plurals.xml diff --git a/build/.htaccess b/build/.htaccess new file mode 100644 index 0000000..e019832 --- /dev/null +++ b/build/.htaccess @@ -0,0 +1 @@ +deny from all diff --git a/build/build b/build/build new file mode 100644 index 0000000..fff4282 --- /dev/null +++ b/build/build @@ -0,0 +1,20 @@ +#!/usr/bin/env php +run(); diff --git a/build/build.bat b/build/build.bat new file mode 100644 index 0000000..a1ae41f --- /dev/null +++ b/build/build.bat @@ -0,0 +1,23 @@ +@echo off + +rem ------------------------------------------------------------- +rem build script for Windows. +rem +rem This is the bootstrap script for running build on Windows. +rem +rem @author Qiang Xue +rem @link http://www.yiiframework.com/ +rem @copyright 2008 Yii Software LLC +rem @license http://www.yiiframework.com/license/ +rem @version $Id$ +rem ------------------------------------------------------------- + +@setlocal + +set BUILD_PATH=%~dp0 + +if "%PHP_COMMAND%" == "" set PHP_COMMAND=php.exe + +%PHP_COMMAND% "%BUILD_PATH%build" %* + +@endlocal \ No newline at end of file diff --git a/build/build.xml b/build/build.xml new file mode 100644 index 0000000..18a420d --- /dev/null +++ b/build/build.xml @@ -0,0 +1,276 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Building package ${pkgname}... + Copying files to build directory... + + + + + Changing file permissions... + + + + + + + + Generating source release file... + + + + + + + + + + + + + + + + + + + + + + + + Building documentation... + + Building Guide PDF... + + + + + + + Building Blog PDF... + + + + + + + Building API... + + + + + Generating doc release file... + + + + + + + + + + + + + + + + Building online API... + + + + Copying tutorials... + + + + + + + + Copying release text files... + + + + + + +Finished building Web files. +Please update yiisite/common/data/versions.php file with the following code: + + '1.1'=>array( + 'version'=>'${yii.version}', + 'revision'=>'${yii.revision}', + 'date'=>'${yii.date}', + 'latest'=>true, + ), + + + + + + Synchronizing code changes for ${pkgname}... + + Building autoload map... + + + Building yiilite.php... + + + + + Extracting i18n messages... + + + + + + + Cleaning up the build... + + + + + + + Welcome to use Yii build script! + -------------------------------- + You may use the following command format to build a target: + + phing <target name> + + where <target name> can be one of the following: + + - sync : synchronize yiilite.php and YiiBase.php + - message : extract i18n messages of the framework + - src : build source release + - doc : build documentation release (Windows only) + - clean : clean up the build + + + + diff --git a/build/controllers/LocaleController.php b/build/controllers/LocaleController.php new file mode 100644 index 0000000..51de6a0 --- /dev/null +++ b/build/controllers/LocaleController.php @@ -0,0 +1,103 @@ + + * @since 2.0 + */ +class LocaleController extends Controller +{ + public $defaultAction = 'plural'; + + /** + * Generates the plural rules data. + * + * This command will parse the plural rule XML file from CLDR and convert them + * into appropriate PHP representation to support Yii message translation feature. + * @param string $xmlFile the original plural rule XML file (from CLDR). This file may be found in + * http://www.unicode.org/Public/cldr/latest/core.zip + * Extract the zip file and locate the file "common/supplemental/plurals.xml". + * @throws Exception + */ + public function actionPlural($xmlFile) + { + if (!is_file($xmlFile)) { + throw new Exception("The source plural rule file does not exist: $xmlFile"); + } + + $xml = simplexml_load_file($xmlFile); + + $allRules = array(); + + $patterns = array( + '/n in 0..1/' => '(n==0||n==1)', + '/\s+is\s+not\s+/i' => '!=', //is not + '/\s+is\s+/i' => '==', //is + '/n\s+mod\s+(\d+)/i' => 'fmod(n,$1)', //mod (CLDR's "mod" is "fmod()", not "%") + '/^(.*?)\s+not\s+(?:in|within)\s+(\d+)\.\.(\d+)/i' => '($1<$2||$1>$3)', //not in, not within + '/^(.*?)\s+within\s+(\d+)\.\.(\d+)/i' => '($1>=$2&&$1<=$3)', //within + '/^(.*?)\s+in\s+(\d+)\.\.(\d+)/i' => '($1>=$2&&$1<=$3&&fmod($1,1)==0)', //in + ); + foreach ($xml->plurals->pluralRules as $node) { + $attributes = $node->attributes(); + $locales = explode(' ', $attributes['locales']); + $rules = array(); + + if (!empty($node->pluralRule)) { + foreach ($node->pluralRule as $rule) { + $expr_or = preg_split('/\s+or\s+/i', $rule); + foreach ($expr_or as $key_or => $val_or) { + $expr_and = preg_split('/\s+and\s+/i', $val_or); + $expr_and = preg_replace(array_keys($patterns), array_values($patterns), $expr_and); + $expr_or[$key_or] = implode('&&', $expr_and); + } + $rules[] = preg_replace('/\\bn\\b/', '$n', implode('||', $expr_or)); + } + foreach ($locales as $locale) { + $allRules[$locale] = $rules; + } + } + } + // hard fix for "br": the rule is too complex + $allRules['br'] = array( + 0 => 'fmod($n,10)==1&&!in_array(fmod($n,100),array(11,71,91))', + 1 => 'fmod($n,10)==2&&!in_array(fmod($n,100),array(12,72,92))', + 2 => 'in_array(fmod($n,10),array(3,4,9))&&!in_array(fmod($n,100),array_merge(range(10,19),range(70,79),range(90,99))))', + 3 => 'fmod($n,1000000)==0&&$n!=0', + ); + if (preg_match('/\d+/', $xml->version['number'], $matches)) { + $revision = $matches[0]; + } else { + $revision = -1; + } + + echo " - * * @since 2.0 */ class Controller extends \yii\base\Controller diff --git a/framework/console/controllers/HelpController.php b/framework/console/controllers/HelpController.php index ea7e3d5..74c354b 100644 --- a/framework/console/controllers/HelpController.php +++ b/framework/console/controllers/HelpController.php @@ -9,9 +9,9 @@ namespace yii\console\controllers; use Yii; use yii\base\Application; -use yii\console\Exception; use yii\base\InlineAction; use yii\console\Controller; +use yii\console\Exception; use yii\console\Request; use yii\helpers\StringHelper; @@ -128,7 +128,7 @@ class HelpController extends Controller $files = scandir($module->getControllerPath()); foreach ($files as $file) { - if(strcmp(substr($file,-14),'Controller.php') === 0 && is_file($file)) { + if (strcmp(substr($file, -14), 'Controller.php') === 0) { $commands[] = $prefix . lcfirst(substr(basename($file), 0, -14)); } } diff --git a/framework/i18n/data/plurals.php b/framework/i18n/data/plurals.php new file mode 100644 index 0000000..3ed5619 --- /dev/null +++ b/framework/i18n/data/plurals.php @@ -0,0 +1,627 @@ + + array ( + 0 => '$n==0', + 1 => '$n==1', + 2 => '$n==2', + 3 => '(fmod($n,100)>=3&&fmod($n,100)<=10&&fmod(fmod($n,100),1)==0)', + 4 => '(fmod($n,100)>=11&&fmod($n,100)<=99&&fmod(fmod($n,100),1)==0)', + ), + 'asa' => + array ( + 0 => '$n==1', + ), + 'af' => + array ( + 0 => '$n==1', + ), + 'bem' => + array ( + 0 => '$n==1', + ), + 'bez' => + array ( + 0 => '$n==1', + ), + 'bg' => + array ( + 0 => '$n==1', + ), + 'bn' => + array ( + 0 => '$n==1', + ), + 'brx' => + array ( + 0 => '$n==1', + ), + 'ca' => + array ( + 0 => '$n==1', + ), + 'cgg' => + array ( + 0 => '$n==1', + ), + 'chr' => + array ( + 0 => '$n==1', + ), + 'da' => + array ( + 0 => '$n==1', + ), + 'de' => + array ( + 0 => '$n==1', + ), + 'dv' => + array ( + 0 => '$n==1', + ), + 'ee' => + array ( + 0 => '$n==1', + ), + 'el' => + array ( + 0 => '$n==1', + ), + 'en' => + array ( + 0 => '$n==1', + ), + 'eo' => + array ( + 0 => '$n==1', + ), + 'es' => + array ( + 0 => '$n==1', + ), + 'et' => + array ( + 0 => '$n==1', + ), + 'eu' => + array ( + 0 => '$n==1', + ), + 'fi' => + array ( + 0 => '$n==1', + ), + 'fo' => + array ( + 0 => '$n==1', + ), + 'fur' => + array ( + 0 => '$n==1', + ), + 'fy' => + array ( + 0 => '$n==1', + ), + 'gl' => + array ( + 0 => '$n==1', + ), + 'gsw' => + array ( + 0 => '$n==1', + ), + 'gu' => + array ( + 0 => '$n==1', + ), + 'ha' => + array ( + 0 => '$n==1', + ), + 'haw' => + array ( + 0 => '$n==1', + ), + 'he' => + array ( + 0 => '$n==1', + ), + 'is' => + array ( + 0 => '$n==1', + ), + 'it' => + array ( + 0 => '$n==1', + ), + 'jmc' => + array ( + 0 => '$n==1', + ), + 'kaj' => + array ( + 0 => '$n==1', + ), + 'kcg' => + array ( + 0 => '$n==1', + ), + 'kk' => + array ( + 0 => '$n==1', + ), + 'kl' => + array ( + 0 => '$n==1', + ), + 'ksb' => + array ( + 0 => '$n==1', + ), + 'ku' => + array ( + 0 => '$n==1', + ), + 'lb' => + array ( + 0 => '$n==1', + ), + 'lg' => + array ( + 0 => '$n==1', + ), + 'mas' => + array ( + 0 => '$n==1', + ), + 'ml' => + array ( + 0 => '$n==1', + ), + 'mn' => + array ( + 0 => '$n==1', + ), + 'mr' => + array ( + 0 => '$n==1', + ), + 'nah' => + array ( + 0 => '$n==1', + ), + 'nb' => + array ( + 0 => '$n==1', + ), + 'nd' => + array ( + 0 => '$n==1', + ), + 'ne' => + array ( + 0 => '$n==1', + ), + 'nl' => + array ( + 0 => '$n==1', + ), + 'nn' => + array ( + 0 => '$n==1', + ), + 'no' => + array ( + 0 => '$n==1', + ), + 'nr' => + array ( + 0 => '$n==1', + ), + 'ny' => + array ( + 0 => '$n==1', + ), + 'nyn' => + array ( + 0 => '$n==1', + ), + 'om' => + array ( + 0 => '$n==1', + ), + 'or' => + array ( + 0 => '$n==1', + ), + 'pa' => + array ( + 0 => '$n==1', + ), + 'pap' => + array ( + 0 => '$n==1', + ), + 'ps' => + array ( + 0 => '$n==1', + ), + 'pt' => + array ( + 0 => '$n==1', + ), + 'rof' => + array ( + 0 => '$n==1', + ), + 'rm' => + array ( + 0 => '$n==1', + ), + 'rwk' => + array ( + 0 => '$n==1', + ), + 'saq' => + array ( + 0 => '$n==1', + ), + 'seh' => + array ( + 0 => '$n==1', + ), + 'sn' => + array ( + 0 => '$n==1', + ), + 'so' => + array ( + 0 => '$n==1', + ), + 'sq' => + array ( + 0 => '$n==1', + ), + 'ss' => + array ( + 0 => '$n==1', + ), + 'ssy' => + array ( + 0 => '$n==1', + ), + 'st' => + array ( + 0 => '$n==1', + ), + 'sv' => + array ( + 0 => '$n==1', + ), + 'sw' => + array ( + 0 => '$n==1', + ), + 'syr' => + array ( + 0 => '$n==1', + ), + 'ta' => + array ( + 0 => '$n==1', + ), + 'te' => + array ( + 0 => '$n==1', + ), + 'teo' => + array ( + 0 => '$n==1', + ), + 'tig' => + array ( + 0 => '$n==1', + ), + 'tk' => + array ( + 0 => '$n==1', + ), + 'tn' => + array ( + 0 => '$n==1', + ), + 'ts' => + array ( + 0 => '$n==1', + ), + 'ur' => + array ( + 0 => '$n==1', + ), + 'wae' => + array ( + 0 => '$n==1', + ), + 've' => + array ( + 0 => '$n==1', + ), + 'vun' => + array ( + 0 => '$n==1', + ), + 'xh' => + array ( + 0 => '$n==1', + ), + 'xog' => + array ( + 0 => '$n==1', + ), + 'zu' => + array ( + 0 => '$n==1', + ), + 'ak' => + array ( + 0 => '($n==0||$n==1)', + ), + 'am' => + array ( + 0 => '($n==0||$n==1)', + ), + 'bh' => + array ( + 0 => '($n==0||$n==1)', + ), + 'fil' => + array ( + 0 => '($n==0||$n==1)', + ), + 'tl' => + array ( + 0 => '($n==0||$n==1)', + ), + 'guw' => + array ( + 0 => '($n==0||$n==1)', + ), + 'hi' => + array ( + 0 => '($n==0||$n==1)', + ), + 'ln' => + array ( + 0 => '($n==0||$n==1)', + ), + 'mg' => + array ( + 0 => '($n==0||$n==1)', + ), + 'nso' => + array ( + 0 => '($n==0||$n==1)', + ), + 'ti' => + array ( + 0 => '($n==0||$n==1)', + ), + 'wa' => + array ( + 0 => '($n==0||$n==1)', + ), + 'ff' => + array ( + 0 => '($n>=0&&$n<=2)&&$n!=2', + ), + 'fr' => + array ( + 0 => '($n>=0&&$n<=2)&&$n!=2', + ), + 'kab' => + array ( + 0 => '($n>=0&&$n<=2)&&$n!=2', + ), + 'lv' => + array ( + 0 => '$n==0', + 1 => 'fmod($n,10)==1&&fmod($n,100)!=11', + ), + 'iu' => + array ( + 0 => '$n==1', + 1 => '$n==2', + ), + 'kw' => + array ( + 0 => '$n==1', + 1 => '$n==2', + ), + 'naq' => + array ( + 0 => '$n==1', + 1 => '$n==2', + ), + 'se' => + array ( + 0 => '$n==1', + 1 => '$n==2', + ), + 'sma' => + array ( + 0 => '$n==1', + 1 => '$n==2', + ), + 'smi' => + array ( + 0 => '$n==1', + 1 => '$n==2', + ), + 'smj' => + array ( + 0 => '$n==1', + 1 => '$n==2', + ), + 'smn' => + array ( + 0 => '$n==1', + 1 => '$n==2', + ), + 'sms' => + array ( + 0 => '$n==1', + 1 => '$n==2', + ), + 'ga' => + array ( + 0 => '$n==1', + 1 => '$n==2', + 2 => '($n>=3&&$n<=6&&fmod($n,1)==0)', + 3 => '($n>=7&&$n<=10&&fmod($n,1)==0)', + ), + 'ro' => + array ( + 0 => '$n==1', + 1 => '$n==0||$n!=1&&(fmod($n,100)>=1&&fmod($n,100)<=19&&fmod(fmod($n,100),1)==0)', + ), + 'mo' => + array ( + 0 => '$n==1', + 1 => '$n==0||$n!=1&&(fmod($n,100)>=1&&fmod($n,100)<=19&&fmod(fmod($n,100),1)==0)', + ), + 'lt' => + array ( + 0 => 'fmod($n,10)==1&&(fmod($n,100)<11||fmod($n,100)>19)', + 1 => '(fmod($n,10)>=2&&fmod($n,10)<=9&&fmod(fmod($n,10),1)==0)&&(fmod($n,100)<11||fmod($n,100)>19)', + ), + 'be' => + array ( + 0 => 'fmod($n,10)==1&&fmod($n,100)!=11', + 1 => '(fmod($n,10)>=2&&fmod($n,10)<=4&&fmod(fmod($n,10),1)==0)&&(fmod($n,100)<12||fmod($n,100)>14)', + 2 => 'fmod($n,10)==0||(fmod($n,10)>=5&&fmod($n,10)<=9&&fmod(fmod($n,10),1)==0)||(fmod($n,100)>=11&&fmod($n,100)<=14&&fmod(fmod($n,100),1)==0)', + ), + 'bs' => + array ( + 0 => 'fmod($n,10)==1&&fmod($n,100)!=11', + 1 => '(fmod($n,10)>=2&&fmod($n,10)<=4&&fmod(fmod($n,10),1)==0)&&(fmod($n,100)<12||fmod($n,100)>14)', + 2 => 'fmod($n,10)==0||(fmod($n,10)>=5&&fmod($n,10)<=9&&fmod(fmod($n,10),1)==0)||(fmod($n,100)>=11&&fmod($n,100)<=14&&fmod(fmod($n,100),1)==0)', + ), + 'hr' => + array ( + 0 => 'fmod($n,10)==1&&fmod($n,100)!=11', + 1 => '(fmod($n,10)>=2&&fmod($n,10)<=4&&fmod(fmod($n,10),1)==0)&&(fmod($n,100)<12||fmod($n,100)>14)', + 2 => 'fmod($n,10)==0||(fmod($n,10)>=5&&fmod($n,10)<=9&&fmod(fmod($n,10),1)==0)||(fmod($n,100)>=11&&fmod($n,100)<=14&&fmod(fmod($n,100),1)==0)', + ), + 'ru' => + array ( + 0 => 'fmod($n,10)==1&&fmod($n,100)!=11', + 1 => '(fmod($n,10)>=2&&fmod($n,10)<=4&&fmod(fmod($n,10),1)==0)&&(fmod($n,100)<12||fmod($n,100)>14)', + 2 => 'fmod($n,10)==0||(fmod($n,10)>=5&&fmod($n,10)<=9&&fmod(fmod($n,10),1)==0)||(fmod($n,100)>=11&&fmod($n,100)<=14&&fmod(fmod($n,100),1)==0)', + ), + 'sh' => + array ( + 0 => 'fmod($n,10)==1&&fmod($n,100)!=11', + 1 => '(fmod($n,10)>=2&&fmod($n,10)<=4&&fmod(fmod($n,10),1)==0)&&(fmod($n,100)<12||fmod($n,100)>14)', + 2 => 'fmod($n,10)==0||(fmod($n,10)>=5&&fmod($n,10)<=9&&fmod(fmod($n,10),1)==0)||(fmod($n,100)>=11&&fmod($n,100)<=14&&fmod(fmod($n,100),1)==0)', + ), + 'sr' => + array ( + 0 => 'fmod($n,10)==1&&fmod($n,100)!=11', + 1 => '(fmod($n,10)>=2&&fmod($n,10)<=4&&fmod(fmod($n,10),1)==0)&&(fmod($n,100)<12||fmod($n,100)>14)', + 2 => 'fmod($n,10)==0||(fmod($n,10)>=5&&fmod($n,10)<=9&&fmod(fmod($n,10),1)==0)||(fmod($n,100)>=11&&fmod($n,100)<=14&&fmod(fmod($n,100),1)==0)', + ), + 'uk' => + array ( + 0 => 'fmod($n,10)==1&&fmod($n,100)!=11', + 1 => '(fmod($n,10)>=2&&fmod($n,10)<=4&&fmod(fmod($n,10),1)==0)&&(fmod($n,100)<12||fmod($n,100)>14)', + 2 => 'fmod($n,10)==0||(fmod($n,10)>=5&&fmod($n,10)<=9&&fmod(fmod($n,10),1)==0)||(fmod($n,100)>=11&&fmod($n,100)<=14&&fmod(fmod($n,100),1)==0)', + ), + 'cs' => + array ( + 0 => '$n==1', + 1 => '($n>=2&&$n<=4&&fmod($n,1)==0)', + ), + 'sk' => + array ( + 0 => '$n==1', + 1 => '($n>=2&&$n<=4&&fmod($n,1)==0)', + ), + 'pl' => + array ( + 0 => '$n==1', + 1 => '(fmod($n,10)>=2&&fmod($n,10)<=4&&fmod(fmod($n,10),1)==0)&&(fmod($n,100)<12||fmod($n,100)>14)', + 2 => '$n!=1&&(fmod($n,10)>=0&&fmod($n,10)<=1&&fmod(fmod($n,10),1)==0)||(fmod($n,10)>=5&&fmod($n,10)<=9&&fmod(fmod($n,10),1)==0)||(fmod($n,100)>=12&&fmod($n,100)<=14&&fmod(fmod($n,100),1)==0)', + ), + 'sl' => + array ( + 0 => 'fmod($n,100)==1', + 1 => 'fmod($n,100)==2', + 2 => '(fmod($n,100)>=3&&fmod($n,100)<=4&&fmod(fmod($n,100),1)==0)', + ), + 'mt' => + array ( + 0 => '$n==1', + 1 => '$n==0||(fmod($n,100)>=2&&fmod($n,100)<=10&&fmod(fmod($n,100),1)==0)', + 2 => '(fmod($n,100)>=11&&fmod($n,100)<=19&&fmod(fmod($n,100),1)==0)', + ), + 'mk' => + array ( + 0 => 'fmod($n,10)==1&&$n!=11', + ), + 'cy' => + array ( + 0 => '$n==0', + 1 => '$n==1', + 2 => '$n==2', + 3 => '$n==3', + 4 => '$n==6', + ), + 'lag' => + array ( + 0 => '$n==0', + 1 => '($n>=0&&$n<=2)&&$n!=0&&$n!=2', + ), + 'shi' => + array ( + 0 => '($n>=0&&$n<=1)', + 1 => '($n>=2&&$n<=10&&fmod($n,1)==0)', + ), + 'br' => + array ( + 0 => 'fmod($n,10)==1&&!in_array(fmod($n,100),array(11,71,91))', + 1 => 'fmod($n,10)==2&&!in_array(fmod($n,100),array(12,72,92))', + 2 => 'in_array(fmod($n,10),array(3,4,9))&&!in_array(fmod($n,100),array_merge(range(10,19),range(70,79),range(90,99))))', + 3 => 'fmod($n,1000000)==0&&$n!=0', + ), + 'ksh' => + array ( + 0 => '$n==0', + 1 => '$n==1', + ), + 'tzm' => + array ( + 0 => '($n==0||$n==1)||($n>=11&&$n<=99&&fmod($n,1)==0)', + ), + 'gv' => + array ( + 0 => '(fmod($n,10)>=1&&fmod($n,10)<=2&&fmod(fmod($n,10),1)==0)||fmod($n,20)==0', + ), +); \ No newline at end of file diff --git a/framework/i18n/data/plurals.xml b/framework/i18n/data/plurals.xml new file mode 100644 index 0000000..9227dc6 --- /dev/null +++ b/framework/i18n/data/plurals.xml @@ -0,0 +1,109 @@ + + + + + + + + + + n is 0 + n is 1 + n is 2 + n mod 100 in 3..10 + n mod 100 in 11..99 + + + n is 1 + + + n in 0..1 + + + n within 0..2 and n is not 2 + + + n is 0 + n mod 10 is 1 and n mod 100 is not 11 + + + n is 1 + n is 2 + + + n is 1 + n is 2 + n in 3..6 + n in 7..10 + + + n is 1 + n is 0 OR n is not 1 AND n mod 100 in 1..19 + + + n mod 10 is 1 and n mod 100 not in 11..19 + n mod 10 in 2..9 and n mod 100 not in 11..19 + + + n mod 10 is 1 and n mod 100 is not 11 + n mod 10 in 2..4 and n mod 100 not in 12..14 + n mod 10 is 0 or n mod 10 in 5..9 or n mod 100 in 11..14 + + + + n is 1 + n in 2..4 + + + n is 1 + n mod 10 in 2..4 and n mod 100 not in 12..14 + n is not 1 and n mod 10 in 0..1 or n mod 10 in 5..9 or n mod 100 in 12..14 + + + + + n mod 100 is 1 + n mod 100 is 2 + n mod 100 in 3..4 + + + n is 1 + n is 0 or n mod 100 in 2..10 + n mod 100 in 11..19 + + + n mod 10 is 1 and n is not 11 + + + n is 0 + n is 1 + n is 2 + n is 3 + n is 6 + + + n is 0 + n within 0..2 and n is not 0 and n is not 2 + + + n within 0..1 + n in 2..10 + + + n mod 10 is 1 and n mod 100 not in 11,71,91 + n mod 10 is 2 and n mod 100 not in 12,72,92 + n mod 10 in 3..4,9 and n mod 100 not in 10..19,70..79,90..99 + n mod 1000000 is 0 and n is not 0 + + + n is 0 + n is 1 + + + n in 0..1 or n in 11..99 + + + n mod 10 in 1..2 or n mod 20 is 0 + + + From c629ad776abe36501fd6c034cff8fc115fdc7f59 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Mon, 1 Apr 2013 17:59:14 -0400 Subject: [PATCH 021/104] Finished plural forms. --- build/build | 0 build/controllers/LocaleController.php | 17 ++++-- framework/YiiBase.php | 39 ++++++++------ framework/base/ErrorHandler.php | 10 ++-- framework/i18n/I18N.php | 97 ++++++++++++++++++++++++++++++---- framework/i18n/data/plurals.php | 66 +++++++++++------------ 6 files changed, 162 insertions(+), 67 deletions(-) mode change 100644 => 100755 build/build diff --git a/build/build b/build/build old mode 100644 new mode 100755 diff --git a/build/controllers/LocaleController.php b/build/controllers/LocaleController.php index 51de6a0..00de787 100644 --- a/build/controllers/LocaleController.php +++ b/build/controllers/LocaleController.php @@ -42,9 +42,10 @@ class LocaleController extends Controller '/\s+is\s+not\s+/i' => '!=', //is not '/\s+is\s+/i' => '==', //is '/n\s+mod\s+(\d+)/i' => 'fmod(n,$1)', //mod (CLDR's "mod" is "fmod()", not "%") - '/^(.*?)\s+not\s+(?:in|within)\s+(\d+)\.\.(\d+)/i' => '($1<$2||$1>$3)', //not in, not within + '/^(.*?)\s+not\s+in\s+(\d+)\.\.(\d+)/i' => '!in_array($1,range($2,$3))', //not in + '/^(.*?)\s+in\s+(\d+)\.\.(\d+)/i' => 'in_array($1,range($2,$3))', //in + '/^(.*?)\s+not\s+within\s+(\d+)\.\.(\d+)/i' => '($1<$2||$1>$3)', //not within '/^(.*?)\s+within\s+(\d+)\.\.(\d+)/i' => '($1>=$2&&$1<=$3)', //within - '/^(.*?)\s+in\s+(\d+)\.\.(\d+)/i' => '($1>=$2&&$1<=$3&&fmod($1,1)==0)', //in ); foreach ($xml->plurals->pluralRules as $node) { $attributes = $node->attributes(); @@ -59,7 +60,15 @@ class LocaleController extends Controller $expr_and = preg_replace(array_keys($patterns), array_values($patterns), $expr_and); $expr_or[$key_or] = implode('&&', $expr_and); } - $rules[] = preg_replace('/\\bn\\b/', '$n', implode('||', $expr_or)); + $expr = preg_replace('/\\bn\\b/', '$n', implode('||', $expr_or)); + $rules[] = preg_replace_callback('/range\((\d+),(\d+)\)/', function ($matches) { + if ($matches[2] - $matches[1] <= 2) { + return 'array(' . implode(',', range($matches[1], $matches[2])) . ')'; + } else { + return $matches[0]; + } + }, $expr); + } foreach ($locales as $locale) { $allRules[$locale] = $rules; @@ -70,7 +79,7 @@ class LocaleController extends Controller $allRules['br'] = array( 0 => 'fmod($n,10)==1&&!in_array(fmod($n,100),array(11,71,91))', 1 => 'fmod($n,10)==2&&!in_array(fmod($n,100),array(12,72,92))', - 2 => 'in_array(fmod($n,10),array(3,4,9))&&!in_array(fmod($n,100),array_merge(range(10,19),range(70,79),range(90,99))))', + 2 => 'in_array(fmod($n,10),array(3,4,9))&&!in_array(fmod($n,100),array_merge(range(10,19),range(70,79),range(90,99)))', 3 => 'fmod($n,1000000)==0&&$n!=0', ); if (preg_match('/\d+/', $xml->version['number'], $matches)) { diff --git a/framework/YiiBase.php b/framework/YiiBase.php index e4a01b4..e81a288 100644 --- a/framework/YiiBase.php +++ b/framework/YiiBase.php @@ -192,6 +192,7 @@ class YiiBase * @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. + * @throws InvalidParamException if the alias is invalid while $throwException is true. * @see setAlias */ public static function getAlias($alias, $throwException = true) @@ -231,6 +232,7 @@ class YiiBase * - a URL (e.g. `http://www.yiiframework.com`) * - a path alias (e.g. `@yii/base`). In this case, the path alias will be converted into the * actual path first by calling [[getAlias]]. + * * @throws Exception if $path is an invalid alias * @see getAlias */ @@ -504,21 +506,28 @@ class YiiBase /** * Translates a message to the specified language. - * This method supports choice format (see {@link CChoiceFormat}), - * i.e., the message returned will be chosen from a few candidates according to the given - * number value. This feature is mainly used to solve plural format issue in case - * a message has different plural forms in some languages. - * @param string $message the original message - * @param array $params parameters to be applied to the message using strtr. - * The first parameter can be a number without key. - * And in this case, the method will call {@link CChoiceFormat::format} to choose - * an appropriate message translation. - * You can pass parameter for {@link CChoiceFormat::format} - * or plural forms format without wrapping it with array. - * @param string $language the target language. If null (default), the {@link CApplication::getLanguage application language} will be used. - * @return string the translated message - * @see CMessageSource - * @see http://www.unicode.org/cldr/charts/supplemental/language_plural_rules.html + * + * The translation will be conducted according to the message category and the target language. + * To specify the category of the message, prefix the message with the category name and separate it + * with "|". For example, "app|hello world". If the category is not specified, the default category "app" + * will be used. The actual message translation is done by a [[\yii\i18n\MessageSource|message source]]. + * + * In case when a translated message has different plural forms (separated by "|"), this method + * will also attempt to choose an appropriate one according to a given numeric value which is + * specified as the first parameter (indexed by 0) in `$params`. + * + * For example, if a translated message is "I have an apple.|I have {n} apples.", and the first + * parameter is 2, the message returned will be "I have 2 apples.". Note that the placeholder "{n}" + * will be replaced with the given number. + * + * For more details on how plural rules are applied, please refer to: + * [[http://www.unicode.org/cldr/charts/supplemental/language_plural_rules.html]] + * + * @param string $message the message to be translated. + * @param array $params the parameters that will be used to replace the corresponding placeholders in the message. + * @param string $language the language code (e.g. `en_US`, `en`). If this is null, the current + * [[\yii\base\Application::language|application language]] will be used. + * @return string the translated message. */ public static function t($message, $params = array(), $language = null) { diff --git a/framework/base/ErrorHandler.php b/framework/base/ErrorHandler.php index f71b8c8..a2f372c 100644 --- a/framework/base/ErrorHandler.php +++ b/framework/base/ErrorHandler.php @@ -16,8 +16,6 @@ namespace yii\base; * @author Qiang Xue * @since 2.0 */ -use yii\helpers\VarDumper; - class ErrorHandler extends Component { /** @@ -63,10 +61,10 @@ class ErrorHandler extends Component $this->clearOutput(); } - $this->render($exception); + $this->renderException($exception); } - protected function render($exception) + protected function renderException($exception) { if ($this->errorAction !== null) { \Yii::$app->runAction($this->errorAction); @@ -84,7 +82,7 @@ class ErrorHandler extends Component } else { $viewName = $this->exceptionView; } - echo $view->render($viewName, array( + echo $view->renderFile($viewName, array( 'exception' => $exception, ), $this); } @@ -255,7 +253,7 @@ class ErrorHandler extends Component { $view = new View; $name = !YII_DEBUG || $exception instanceof HttpException ? $this->errorView : $this->exceptionView; - echo $view->render($name, array( + echo $view->renderFile($name, array( 'exception' => $exception, ), $this); } diff --git a/framework/i18n/I18N.php b/framework/i18n/I18N.php index 0409da3..8667abc 100644 --- a/framework/i18n/I18N.php +++ b/framework/i18n/I18N.php @@ -1,11 +1,23 @@ + * @since 2.0 + */ class I18N extends Component { /** @@ -13,11 +25,36 @@ class I18N extends Component * 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'. + * + * This property may be modified on the fly by extensions who want to have their own message sources + * registered under their own namespaces. + * + * The category "yii" and "app" are always defined. The former refers to the messages used in the Yii core + * framework code, while the latter refers to the default message category for custom application code. + * By default, both of these categories use [[PhpMessageSource]] and the corresponding message files are + * stored under "@yii/messages" and "@app/messages", respectively. + * + * You may override the configuration of both categories. */ public $translations; + /** + * @var string the path or path alias of the file that contains the plural rules. + * By default, this refers to a file shipped with the Yii distribution. The file is obtained + * by converting from the data file in the CLDR project. + * + * If the default rule file does not contain the expected rules, you may copy and modify it + * for your application, and then configure this property to point to your modified copy. + * + * @see http://www.unicode.org/cldr/charts/supplemental/language_plural_rules.html + */ + public $pluralRuleFile = '@yii/i18n/data/plurals.php'; + /** + * Initializes the component by configuring the default message categories. + */ public function init() { + parent::init(); if (!isset($this->translations['yii'])) { $this->translations['yii'] = array( 'class' => 'yii\i18n\PhpMessageSource', @@ -34,6 +71,16 @@ class I18N extends Component } } + /** + * Translates a message to the specified language. + * If the first parameter in `$params` is a number and it is indexed by 0, appropriate plural rules + * will be applied to the translated message. + * @param string $message the message to be translated. + * @param array $params the parameters that will be used to replace the corresponding placeholders in the message. + * @param string $language the language code (e.g. `en_US`, `en`). If this is null, the current + * [[\yii\base\Application::language|application language]] will be used. + * @return string the translated message. + */ public function translate($message, $params = array(), $language = null) { if ($language === null) { @@ -55,7 +102,7 @@ class I18N extends Component } if (isset($params[0])) { - $message = $this->getPluralForm($message, $params[0], $language); + $message = $this->applyPluralRules($message, $params[0], $language); if (!isset($params['{n}'])) { $params['{n}'] = $params[0]; } @@ -65,6 +112,12 @@ class I18N extends Component return $params === array() ? $message : strtr($message, $params); } + /** + * Returns the message source for the given category. + * @param string $category the category name. + * @return MessageSource the message source for the given category. + * @throws InvalidConfigException if there is no message source available for the specified category. + */ public function getMessageSource($category) { if (isset($this->translations[$category])) { @@ -85,18 +138,21 @@ class I18N extends Component } } - public function getLocale($language) - { - - } - - protected function getPluralForm($message, $number, $language) + /** + * Applies appropriate plural rules to the given message. + * @param string $message the message to be applied with plural rules + * @param mixed $number the number by which plural rules will be applied + * @param string $language the language code that determines which set of plural rules to be applied. + * @return string the message that has applied plural rules + */ + protected function applyPluralRules($message, $number, $language) { if (strpos($message, '|') === false) { return $message; } $chunks = explode('|', $message); - $rules = $this->getLocale($language)->getPluralRules(); + + $rules = $this->getPluralRules($language); foreach ($rules as $i => $rule) { if (isset($chunks[$i]) && $this->evaluate($rule, $number)) { return $chunks[$i]; @@ -106,6 +162,29 @@ class I18N extends Component return isset($chunks[$n]) ? $chunks[$n] : $chunks[0]; } + private $_pluralRules = array(); // language => rule set + + /** + * Returns the plural rules for the given language code. + * @param string $language the language code (e.g. `en_US`, `en`). + * @return array the plural rules + * @throws InvalidParamException if the language code is invalid. + */ + protected function getPluralRules($language) + { + if (isset($this->_pluralRules[$language])) { + return $this->_pluralRules; + } + $allRules = require(Yii::getAlias($this->pluralRuleFile)); + if (isset($allRules[$language])) { + return $this->_pluralRules[$language] = $allRules[$language]; + } elseif (preg_match('/^[a-z]+/', strtolower($language), $matches)) { + return $this->_pluralRules[$language] = isset($allRules[$matches[0]]) ? $allRules[$matches[0]] : array(); + } else { + throw new InvalidParamException("Invalid language code: $language"); + } + } + /** * Evaluates a PHP expression with the given number value. * @param string $expression the PHP expression @@ -114,6 +193,6 @@ class I18N extends Component */ protected function evaluate($expression, $n) { - return @eval("return $expression;"); + return eval("return $expression;"); } } diff --git a/framework/i18n/data/plurals.php b/framework/i18n/data/plurals.php index 3ed5619..b82fec0 100644 --- a/framework/i18n/data/plurals.php +++ b/framework/i18n/data/plurals.php @@ -21,8 +21,8 @@ return array ( 0 => '$n==0', 1 => '$n==1', 2 => '$n==2', - 3 => '(fmod($n,100)>=3&&fmod($n,100)<=10&&fmod(fmod($n,100),1)==0)', - 4 => '(fmod($n,100)>=11&&fmod($n,100)<=99&&fmod(fmod($n,100),1)==0)', + 3 => 'in_array(fmod($n,100),range(3,10))', + 4 => 'in_array(fmod($n,100),range(11,99))', ), 'asa' => array ( @@ -494,93 +494,93 @@ return array ( array ( 0 => '$n==1', 1 => '$n==2', - 2 => '($n>=3&&$n<=6&&fmod($n,1)==0)', - 3 => '($n>=7&&$n<=10&&fmod($n,1)==0)', + 2 => 'in_array($n,range(3,6))', + 3 => 'in_array($n,range(7,10))', ), 'ro' => array ( 0 => '$n==1', - 1 => '$n==0||$n!=1&&(fmod($n,100)>=1&&fmod($n,100)<=19&&fmod(fmod($n,100),1)==0)', + 1 => '$n==0||$n!=1&&in_array(fmod($n,100),range(1,19))', ), 'mo' => array ( 0 => '$n==1', - 1 => '$n==0||$n!=1&&(fmod($n,100)>=1&&fmod($n,100)<=19&&fmod(fmod($n,100),1)==0)', + 1 => '$n==0||$n!=1&&in_array(fmod($n,100),range(1,19))', ), 'lt' => array ( - 0 => 'fmod($n,10)==1&&(fmod($n,100)<11||fmod($n,100)>19)', - 1 => '(fmod($n,10)>=2&&fmod($n,10)<=9&&fmod(fmod($n,10),1)==0)&&(fmod($n,100)<11||fmod($n,100)>19)', + 0 => 'fmod($n,10)==1&&!in_array(fmod($n,100),range(11,19))', + 1 => 'in_array(fmod($n,10),range(2,9))&&!in_array(fmod($n,100),range(11,19))', ), 'be' => array ( 0 => 'fmod($n,10)==1&&fmod($n,100)!=11', - 1 => '(fmod($n,10)>=2&&fmod($n,10)<=4&&fmod(fmod($n,10),1)==0)&&(fmod($n,100)<12||fmod($n,100)>14)', - 2 => 'fmod($n,10)==0||(fmod($n,10)>=5&&fmod($n,10)<=9&&fmod(fmod($n,10),1)==0)||(fmod($n,100)>=11&&fmod($n,100)<=14&&fmod(fmod($n,100),1)==0)', + 1 => 'in_array(fmod($n,10),array(2,3,4))&&!in_array(fmod($n,100),array(12,13,14))', + 2 => 'fmod($n,10)==0||in_array(fmod($n,10),range(5,9))||in_array(fmod($n,100),range(11,14))', ), 'bs' => array ( 0 => 'fmod($n,10)==1&&fmod($n,100)!=11', - 1 => '(fmod($n,10)>=2&&fmod($n,10)<=4&&fmod(fmod($n,10),1)==0)&&(fmod($n,100)<12||fmod($n,100)>14)', - 2 => 'fmod($n,10)==0||(fmod($n,10)>=5&&fmod($n,10)<=9&&fmod(fmod($n,10),1)==0)||(fmod($n,100)>=11&&fmod($n,100)<=14&&fmod(fmod($n,100),1)==0)', + 1 => 'in_array(fmod($n,10),array(2,3,4))&&!in_array(fmod($n,100),array(12,13,14))', + 2 => 'fmod($n,10)==0||in_array(fmod($n,10),range(5,9))||in_array(fmod($n,100),range(11,14))', ), 'hr' => array ( 0 => 'fmod($n,10)==1&&fmod($n,100)!=11', - 1 => '(fmod($n,10)>=2&&fmod($n,10)<=4&&fmod(fmod($n,10),1)==0)&&(fmod($n,100)<12||fmod($n,100)>14)', - 2 => 'fmod($n,10)==0||(fmod($n,10)>=5&&fmod($n,10)<=9&&fmod(fmod($n,10),1)==0)||(fmod($n,100)>=11&&fmod($n,100)<=14&&fmod(fmod($n,100),1)==0)', + 1 => 'in_array(fmod($n,10),array(2,3,4))&&!in_array(fmod($n,100),array(12,13,14))', + 2 => 'fmod($n,10)==0||in_array(fmod($n,10),range(5,9))||in_array(fmod($n,100),range(11,14))', ), 'ru' => array ( 0 => 'fmod($n,10)==1&&fmod($n,100)!=11', - 1 => '(fmod($n,10)>=2&&fmod($n,10)<=4&&fmod(fmod($n,10),1)==0)&&(fmod($n,100)<12||fmod($n,100)>14)', - 2 => 'fmod($n,10)==0||(fmod($n,10)>=5&&fmod($n,10)<=9&&fmod(fmod($n,10),1)==0)||(fmod($n,100)>=11&&fmod($n,100)<=14&&fmod(fmod($n,100),1)==0)', + 1 => 'in_array(fmod($n,10),array(2,3,4))&&!in_array(fmod($n,100),array(12,13,14))', + 2 => 'fmod($n,10)==0||in_array(fmod($n,10),range(5,9))||in_array(fmod($n,100),range(11,14))', ), 'sh' => array ( 0 => 'fmod($n,10)==1&&fmod($n,100)!=11', - 1 => '(fmod($n,10)>=2&&fmod($n,10)<=4&&fmod(fmod($n,10),1)==0)&&(fmod($n,100)<12||fmod($n,100)>14)', - 2 => 'fmod($n,10)==0||(fmod($n,10)>=5&&fmod($n,10)<=9&&fmod(fmod($n,10),1)==0)||(fmod($n,100)>=11&&fmod($n,100)<=14&&fmod(fmod($n,100),1)==0)', + 1 => 'in_array(fmod($n,10),array(2,3,4))&&!in_array(fmod($n,100),array(12,13,14))', + 2 => 'fmod($n,10)==0||in_array(fmod($n,10),range(5,9))||in_array(fmod($n,100),range(11,14))', ), 'sr' => array ( 0 => 'fmod($n,10)==1&&fmod($n,100)!=11', - 1 => '(fmod($n,10)>=2&&fmod($n,10)<=4&&fmod(fmod($n,10),1)==0)&&(fmod($n,100)<12||fmod($n,100)>14)', - 2 => 'fmod($n,10)==0||(fmod($n,10)>=5&&fmod($n,10)<=9&&fmod(fmod($n,10),1)==0)||(fmod($n,100)>=11&&fmod($n,100)<=14&&fmod(fmod($n,100),1)==0)', + 1 => 'in_array(fmod($n,10),array(2,3,4))&&!in_array(fmod($n,100),array(12,13,14))', + 2 => 'fmod($n,10)==0||in_array(fmod($n,10),range(5,9))||in_array(fmod($n,100),range(11,14))', ), 'uk' => array ( 0 => 'fmod($n,10)==1&&fmod($n,100)!=11', - 1 => '(fmod($n,10)>=2&&fmod($n,10)<=4&&fmod(fmod($n,10),1)==0)&&(fmod($n,100)<12||fmod($n,100)>14)', - 2 => 'fmod($n,10)==0||(fmod($n,10)>=5&&fmod($n,10)<=9&&fmod(fmod($n,10),1)==0)||(fmod($n,100)>=11&&fmod($n,100)<=14&&fmod(fmod($n,100),1)==0)', + 1 => 'in_array(fmod($n,10),array(2,3,4))&&!in_array(fmod($n,100),array(12,13,14))', + 2 => 'fmod($n,10)==0||in_array(fmod($n,10),range(5,9))||in_array(fmod($n,100),range(11,14))', ), 'cs' => array ( 0 => '$n==1', - 1 => '($n>=2&&$n<=4&&fmod($n,1)==0)', + 1 => 'in_array($n,array(2,3,4))', ), 'sk' => array ( 0 => '$n==1', - 1 => '($n>=2&&$n<=4&&fmod($n,1)==0)', + 1 => 'in_array($n,array(2,3,4))', ), 'pl' => array ( 0 => '$n==1', - 1 => '(fmod($n,10)>=2&&fmod($n,10)<=4&&fmod(fmod($n,10),1)==0)&&(fmod($n,100)<12||fmod($n,100)>14)', - 2 => '$n!=1&&(fmod($n,10)>=0&&fmod($n,10)<=1&&fmod(fmod($n,10),1)==0)||(fmod($n,10)>=5&&fmod($n,10)<=9&&fmod(fmod($n,10),1)==0)||(fmod($n,100)>=12&&fmod($n,100)<=14&&fmod(fmod($n,100),1)==0)', + 1 => 'in_array(fmod($n,10),array(2,3,4))&&!in_array(fmod($n,100),array(12,13,14))', + 2 => '$n!=1&&in_array(fmod($n,10),array(0,1))||in_array(fmod($n,10),range(5,9))||in_array(fmod($n,100),array(12,13,14))', ), 'sl' => array ( 0 => 'fmod($n,100)==1', 1 => 'fmod($n,100)==2', - 2 => '(fmod($n,100)>=3&&fmod($n,100)<=4&&fmod(fmod($n,100),1)==0)', + 2 => 'in_array(fmod($n,100),array(3,4))', ), 'mt' => array ( 0 => '$n==1', - 1 => '$n==0||(fmod($n,100)>=2&&fmod($n,100)<=10&&fmod(fmod($n,100),1)==0)', - 2 => '(fmod($n,100)>=11&&fmod($n,100)<=19&&fmod(fmod($n,100),1)==0)', + 1 => '$n==0||in_array(fmod($n,100),range(2,10))', + 2 => 'in_array(fmod($n,100),range(11,19))', ), 'mk' => array ( @@ -602,13 +602,13 @@ return array ( 'shi' => array ( 0 => '($n>=0&&$n<=1)', - 1 => '($n>=2&&$n<=10&&fmod($n,1)==0)', + 1 => 'in_array($n,range(2,10))', ), 'br' => array ( 0 => 'fmod($n,10)==1&&!in_array(fmod($n,100),array(11,71,91))', 1 => 'fmod($n,10)==2&&!in_array(fmod($n,100),array(12,72,92))', - 2 => 'in_array(fmod($n,10),array(3,4,9))&&!in_array(fmod($n,100),array_merge(range(10,19),range(70,79),range(90,99))))', + 2 => 'in_array(fmod($n,10),array(3,4,9))&&!in_array(fmod($n,100),array_merge(range(10,19),range(70,79),range(90,99)))', 3 => 'fmod($n,1000000)==0&&$n!=0', ), 'ksh' => @@ -618,10 +618,10 @@ return array ( ), 'tzm' => array ( - 0 => '($n==0||$n==1)||($n>=11&&$n<=99&&fmod($n,1)==0)', + 0 => '($n==0||$n==1)||in_array($n,range(11,99))', ), 'gv' => array ( - 0 => '(fmod($n,10)>=1&&fmod($n,10)<=2&&fmod(fmod($n,10),1)==0)||fmod($n,20)==0', + 0 => 'in_array(fmod($n,10),array(1,2))||fmod($n,20)==0', ), ); \ No newline at end of file From 0e8e94bcb9079adbcbc37a6d5020a4372fedaf0a Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Tue, 2 Apr 2013 17:59:54 -0400 Subject: [PATCH 022/104] cleanup. --- build/controllers/LocaleController.php | 2 +- framework/i18n/data/plurals.php | 20 +++++++++--------- framework/widgets/ActiveForm.php | 38 +++++++++++++++++++++++++++------- 3 files changed, 42 insertions(+), 18 deletions(-) diff --git a/build/controllers/LocaleController.php b/build/controllers/LocaleController.php index 00de787..d471c0d 100644 --- a/build/controllers/LocaleController.php +++ b/build/controllers/LocaleController.php @@ -62,7 +62,7 @@ class LocaleController extends Controller } $expr = preg_replace('/\\bn\\b/', '$n', implode('||', $expr_or)); $rules[] = preg_replace_callback('/range\((\d+),(\d+)\)/', function ($matches) { - if ($matches[2] - $matches[1] <= 2) { + if ($matches[2] - $matches[1] <= 5) { return 'array(' . implode(',', range($matches[1], $matches[2])) . ')'; } else { return $matches[0]; diff --git a/framework/i18n/data/plurals.php b/framework/i18n/data/plurals.php index b82fec0..52c733b 100644 --- a/framework/i18n/data/plurals.php +++ b/framework/i18n/data/plurals.php @@ -494,8 +494,8 @@ return array ( array ( 0 => '$n==1', 1 => '$n==2', - 2 => 'in_array($n,range(3,6))', - 3 => 'in_array($n,range(7,10))', + 2 => 'in_array($n,array(3,4,5,6))', + 3 => 'in_array($n,array(7,8,9,10))', ), 'ro' => array ( @@ -516,43 +516,43 @@ return array ( array ( 0 => 'fmod($n,10)==1&&fmod($n,100)!=11', 1 => 'in_array(fmod($n,10),array(2,3,4))&&!in_array(fmod($n,100),array(12,13,14))', - 2 => 'fmod($n,10)==0||in_array(fmod($n,10),range(5,9))||in_array(fmod($n,100),range(11,14))', + 2 => 'fmod($n,10)==0||in_array(fmod($n,10),array(5,6,7,8,9))||in_array(fmod($n,100),array(11,12,13,14))', ), 'bs' => array ( 0 => 'fmod($n,10)==1&&fmod($n,100)!=11', 1 => 'in_array(fmod($n,10),array(2,3,4))&&!in_array(fmod($n,100),array(12,13,14))', - 2 => 'fmod($n,10)==0||in_array(fmod($n,10),range(5,9))||in_array(fmod($n,100),range(11,14))', + 2 => 'fmod($n,10)==0||in_array(fmod($n,10),array(5,6,7,8,9))||in_array(fmod($n,100),array(11,12,13,14))', ), 'hr' => array ( 0 => 'fmod($n,10)==1&&fmod($n,100)!=11', 1 => 'in_array(fmod($n,10),array(2,3,4))&&!in_array(fmod($n,100),array(12,13,14))', - 2 => 'fmod($n,10)==0||in_array(fmod($n,10),range(5,9))||in_array(fmod($n,100),range(11,14))', + 2 => 'fmod($n,10)==0||in_array(fmod($n,10),array(5,6,7,8,9))||in_array(fmod($n,100),array(11,12,13,14))', ), 'ru' => array ( 0 => 'fmod($n,10)==1&&fmod($n,100)!=11', 1 => 'in_array(fmod($n,10),array(2,3,4))&&!in_array(fmod($n,100),array(12,13,14))', - 2 => 'fmod($n,10)==0||in_array(fmod($n,10),range(5,9))||in_array(fmod($n,100),range(11,14))', + 2 => 'fmod($n,10)==0||in_array(fmod($n,10),array(5,6,7,8,9))||in_array(fmod($n,100),array(11,12,13,14))', ), 'sh' => array ( 0 => 'fmod($n,10)==1&&fmod($n,100)!=11', 1 => 'in_array(fmod($n,10),array(2,3,4))&&!in_array(fmod($n,100),array(12,13,14))', - 2 => 'fmod($n,10)==0||in_array(fmod($n,10),range(5,9))||in_array(fmod($n,100),range(11,14))', + 2 => 'fmod($n,10)==0||in_array(fmod($n,10),array(5,6,7,8,9))||in_array(fmod($n,100),array(11,12,13,14))', ), 'sr' => array ( 0 => 'fmod($n,10)==1&&fmod($n,100)!=11', 1 => 'in_array(fmod($n,10),array(2,3,4))&&!in_array(fmod($n,100),array(12,13,14))', - 2 => 'fmod($n,10)==0||in_array(fmod($n,10),range(5,9))||in_array(fmod($n,100),range(11,14))', + 2 => 'fmod($n,10)==0||in_array(fmod($n,10),array(5,6,7,8,9))||in_array(fmod($n,100),array(11,12,13,14))', ), 'uk' => array ( 0 => 'fmod($n,10)==1&&fmod($n,100)!=11', 1 => 'in_array(fmod($n,10),array(2,3,4))&&!in_array(fmod($n,100),array(12,13,14))', - 2 => 'fmod($n,10)==0||in_array(fmod($n,10),range(5,9))||in_array(fmod($n,100),range(11,14))', + 2 => 'fmod($n,10)==0||in_array(fmod($n,10),array(5,6,7,8,9))||in_array(fmod($n,100),array(11,12,13,14))', ), 'cs' => array ( @@ -568,7 +568,7 @@ return array ( array ( 0 => '$n==1', 1 => 'in_array(fmod($n,10),array(2,3,4))&&!in_array(fmod($n,100),array(12,13,14))', - 2 => '$n!=1&&in_array(fmod($n,10),array(0,1))||in_array(fmod($n,10),range(5,9))||in_array(fmod($n,100),array(12,13,14))', + 2 => '$n!=1&&in_array(fmod($n,10),array(0,1))||in_array(fmod($n,10),array(5,6,7,8,9))||in_array(fmod($n,100),array(12,13,14))', ), 'sl' => array ( diff --git a/framework/widgets/ActiveForm.php b/framework/widgets/ActiveForm.php index 2c965e7..8ac5365 100644 --- a/framework/widgets/ActiveForm.php +++ b/framework/widgets/ActiveForm.php @@ -110,8 +110,7 @@ class ActiveForm extends Widget */ public function error($model, $attribute, $options = array()) { - $attribute = $this->normalizeAttributeName($attribute); - $this->getInputName($model, $attribute); + $attribute = $this->getAttributeName($attribute); $tag = isset($options['tag']) ? $options['tag'] : 'div'; unset($options['tag']); $error = $model->getFirstError($attribute); @@ -126,15 +125,19 @@ class ActiveForm extends Widget */ 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); + $attribute = $this->getAttributeName($attribute); + $label = isset($options['label']) ? $options['label'] : Html::encode($model->getAttributeLabel($attribute)); + $for = array_key_exists('for', $options) ? $options['for'] : $this->getInputId($model, $attribute); + return Html::label($label, $for, $options); } public function input($type, $model, $attribute, $options = array()) { $value = $this->getAttributeValue($model, $attribute); $name = $this->getInputName($model, $attribute); + if (!array_key_exists('id', $options)) { + $options['id'] = $this->getInputId($model, $attribute); + } return Html::input($type, $name, $value, $options); } @@ -162,6 +165,9 @@ class ActiveForm extends Widget { $value = $this->getAttributeValue($model, $attribute); $name = $this->getInputName($model, $attribute); + if (!array_key_exists('id', $options)) { + $options['id'] = $this->getInputId($model, $attribute); + } return Html::textarea($name, $value, $options); } @@ -172,6 +178,9 @@ class ActiveForm extends Widget if (!array_key_exists('uncheck', $options)) { $options['unchecked'] = '0'; } + if (!array_key_exists('id', $options)) { + $options['id'] = $this->getInputId($model, $attribute); + } return Html::radio($name, $checked, $value, $options); } @@ -182,6 +191,9 @@ class ActiveForm extends Widget if (!array_key_exists('uncheck', $options)) { $options['unchecked'] = '0'; } + if (!array_key_exists('id', $options)) { + $options['id'] = $this->getInputId($model, $attribute); + } return Html::checkbox($name, $checked, $value, $options); } @@ -189,6 +201,9 @@ class ActiveForm extends Widget { $checked = $this->getAttributeValue($model, $attribute); $name = $this->getInputName($model, $attribute); + if (!array_key_exists('id', $options)) { + $options['id'] = $this->getInputId($model, $attribute); + } return Html::dropDownList($name, $checked, $items, $options); } @@ -199,6 +214,9 @@ class ActiveForm extends Widget if (!array_key_exists('unselect', $options)) { $options['unselect'] = '0'; } + if (!array_key_exists('id', $options)) { + $options['id'] = $this->getInputId($model, $attribute); + } return Html::listBox($name, $checked, $items, $options); } @@ -228,7 +246,7 @@ class ActiveForm extends Widget if (isset($this->modelMap[$class])) { $class = $this->modelMap[$class]; } elseif (($pos = strrpos($class, '\\')) !== false) { - $class = substr($class, $pos); + $class = substr($class, $pos + 1); } if (!preg_match('/(^|.*\])(\w+)(\[.*|$)/', $attribute, $matches)) { throw new InvalidParamException('Attribute name must contain word characters only.'); @@ -245,6 +263,12 @@ class ActiveForm extends Widget } } + public function getInputId($model, $attribute) + { + $name = $this->getInputName($model, $attribute); + return str_replace(array('[]', '][', '[', ']', ' '), array('', '-', '-', '', '-'), $name); + } + public function getAttributeValue($model, $attribute) { if (!preg_match('/(^|.*\])(\w+)(\[.*|$)/', $attribute, $matches)) { @@ -267,7 +291,7 @@ class ActiveForm extends Widget } } - public function normalizeAttributeName($attribute) + public function getAttributeName($attribute) { if (preg_match('/(^|.*\])(\w+)(\[.*|$)/', $attribute, $matches)) { return $matches[2]; From 4fe13c937f139a7537893ccccb0ed7ebe76032c8 Mon Sep 17 00:00:00 2001 From: resurtm Date: Wed, 3 Apr 2013 23:10:54 +0600 Subject: [PATCH 023/104] ChainedDependency typo fix. --- framework/caching/ChainedDependency.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework/caching/ChainedDependency.php b/framework/caching/ChainedDependency.php index 9c4e547..af34e9d 100644 --- a/framework/caching/ChainedDependency.php +++ b/framework/caching/ChainedDependency.php @@ -57,7 +57,7 @@ class ChainedDependency extends Dependency if (!$dependency instanceof Dependency) { $dependency = \Yii::createObject($dependency); } - $dependency->evalulateDependency(); + $dependency->evaluateDependency(); } } From aad6e6c8ce17cf2ea239d556b34312aab5886c76 Mon Sep 17 00:00:00 2001 From: resurtm Date: Thu, 4 Apr 2013 00:01:09 +0600 Subject: [PATCH 024/104] Unused `use` has been removed. --- framework/widgets/FragmentCache.php | 1 - 1 file changed, 1 deletion(-) diff --git a/framework/widgets/FragmentCache.php b/framework/widgets/FragmentCache.php index dae538a..637d115 100644 --- a/framework/widgets/FragmentCache.php +++ b/framework/widgets/FragmentCache.php @@ -8,7 +8,6 @@ namespace yii\widgets; use Yii; -use yii\base\InvalidConfigException; use yii\base\Widget; use yii\caching\Cache; use yii\caching\Dependency; From b9a835518a581ccf517d163ad662d322e13e7fc3 Mon Sep 17 00:00:00 2001 From: resurtm Date: Thu, 4 Apr 2013 00:05:09 +0600 Subject: [PATCH 025/104] Fixes issue related to the cache dependencies constructor parameters. (Discussed via Skype with Qiang.) --- framework/caching/DbDependency.php | 18 ++++++++---------- framework/caching/ExpressionDependency.php | 13 +------------ framework/caching/FileDependency.php | 16 +++++++++------- 3 files changed, 18 insertions(+), 29 deletions(-) diff --git a/framework/caching/DbDependency.php b/framework/caching/DbDependency.php index cbe0ae1..4308dc1 100644 --- a/framework/caching/DbDependency.php +++ b/framework/caching/DbDependency.php @@ -28,25 +28,23 @@ class DbDependency extends Dependency public $db = 'db'; /** * @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. + * Only the first row of the query result will be used. This property must be always set, otherwise + * an exception would be raised. */ public $sql; /** * @var array the parameters (name=>value) to be bound to the SQL statement specified by [[sql]]. */ - public $params; + public $params = array(); /** - * Constructor. - * @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 + * Initializes the database dependency object. */ - public function __construct($sql, $params = array(), $config = array()) + public function init() { - $this->sql = $sql; - $this->params = $params; - parent::__construct($config); + if ($this->sql === null) { + throw new InvalidConfigException('DbDependency::sql must be set.'); + } } /** diff --git a/framework/caching/ExpressionDependency.php b/framework/caching/ExpressionDependency.php index e13c962..bf70291 100644 --- a/framework/caching/ExpressionDependency.php +++ b/framework/caching/ExpressionDependency.php @@ -22,18 +22,7 @@ class ExpressionDependency extends Dependency /** * @var string the PHP expression whose result is used to determine the dependency. */ - public $expression; - - /** - * Constructor. - * @param string $expression the PHP expression whose result is used to determine the dependency. - * @param array $config name-value pairs that will be used to initialize the object properties - */ - public function __construct($expression = 'true', $config = array()) - { - $this->expression = $expression; - parent::__construct($config); - } + public $expression = 'true'; /** * Generates the data needed to determine if dependency has been changed. diff --git a/framework/caching/FileDependency.php b/framework/caching/FileDependency.php index 3797dde..8d858ec 100644 --- a/framework/caching/FileDependency.php +++ b/framework/caching/FileDependency.php @@ -7,6 +7,8 @@ namespace yii\caching; +use yii\base\InvalidConfigException; + /** * FileDependency represents a dependency based on a file's last modification time. * @@ -20,19 +22,19 @@ class FileDependency extends Dependency { /** * @var string the name of the file whose last modification time is used to - * check if the dependency has been changed. + * check if the dependency has been changed. This property must be always set, + * otherwise an exception would be raised. */ public $fileName; /** - * Constructor. - * @param string $fileName name of the file whose change is to be checked. - * @param array $config name-value pairs that will be used to initialize the object properties + * Initializes the database dependency object. */ - public function __construct($fileName = null, $config = array()) + public function init() { - $this->fileName = $fileName; - parent::__construct($config); + if ($this->file === null) { + throw new InvalidConfigException('FileDependency::fileName must be set.'); + } } /** From 62fd92bf72334277e59ac0b07806c0d4dc4774be Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Wed, 3 Apr 2013 14:51:15 -0400 Subject: [PATCH 026/104] validator wip --- framework/validators/BooleanValidator.php | 9 +++++++++ framework/validators/Validator.php | 8 +++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/framework/validators/BooleanValidator.php b/framework/validators/BooleanValidator.php index 427fa44..7a7b68f 100644 --- a/framework/validators/BooleanValidator.php +++ b/framework/validators/BooleanValidator.php @@ -60,6 +60,15 @@ class BooleanValidator extends Validator } } + public function validateValue($value) + { + if ($this->allowEmpty && $this->isEmpty($value)) { + return; + } + return ($this->strict || $value == $this->trueValue || $value == $this->falseValue) + && (!$this->strict || $value === $this->trueValue || $value === $this->falseValue); + } + /** * Returns the JavaScript needed for performing client-side validation. * @param \yii\base\Model $object the data object being validated diff --git a/framework/validators/Validator.php b/framework/validators/Validator.php index b688f32..00a88ba 100644 --- a/framework/validators/Validator.php +++ b/framework/validators/Validator.php @@ -8,6 +8,7 @@ namespace yii\validators; use yii\base\Component; +use yii\base\NotSupportedException; /** * Validator is the base class for all validators. @@ -81,7 +82,7 @@ abstract class Validator extends Component */ public $message; /** - * @var array list of scenarios that the validator should be applied. + * @var array list of scenarios that the validator can be applied to. */ public $on = array(); /** @@ -174,6 +175,11 @@ abstract class Validator extends Component } } + public function validateValue($value) + { + throw new NotSupportedException(__CLASS__ . ' does not support validateValue().'); + } + /** * Returns the JavaScript needed for performing client-side validation. * From 6519da3c925521b6c11e78939e4de3c9d2747be7 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Wed, 3 Apr 2013 15:15:03 -0400 Subject: [PATCH 027/104] reverted back the previous changes, and fixed ChainedDependency. --- framework/caching/ChainedDependency.php | 20 ++++++-------------- framework/caching/DbDependency.php | 18 ++++++++++-------- framework/caching/ExpressionDependency.php | 13 ++++++++++++- framework/caching/FileDependency.php | 16 +++++++--------- 4 files changed, 35 insertions(+), 32 deletions(-) diff --git a/framework/caching/ChainedDependency.php b/framework/caching/ChainedDependency.php index af34e9d..7c7058e 100644 --- a/framework/caching/ChainedDependency.php +++ b/framework/caching/ChainedDependency.php @@ -22,11 +22,10 @@ namespace yii\caching; class ChainedDependency extends Dependency { /** - * @var array list of dependencies that this dependency is composed of. - * Each array element should be a dependency object or a configuration array - * that can be used to create a dependency object via [[\Yii::createObject()]]. + * @var Dependency[] list of dependencies that this dependency is composed of. + * Each array element must be a dependency object. */ - public $dependencies = array(); + public $dependencies; /** * @var boolean whether this dependency is depending on every dependency in [[dependencies]]. * Defaults to true, meaning if any of the dependencies has changed, this dependency is considered changed. @@ -37,9 +36,8 @@ class ChainedDependency extends Dependency /** * Constructor. - * @param array $dependencies list of dependencies that this dependency is composed of. - * Each array element should be a dependency object or a configuration array - * that can be used to create a dependency object via [[\Yii::createObject()]]. + * @param Dependency[] $dependencies list of dependencies that this dependency is composed of. + * Each array element should be a dependency object. * @param array $config name-value pairs that will be used to initialize the object properties */ public function __construct($dependencies = array(), $config = array()) @@ -54,9 +52,6 @@ class ChainedDependency extends Dependency public function evaluateDependency() { foreach ($this->dependencies as $dependency) { - if (!$dependency instanceof Dependency) { - $dependency = \Yii::createObject($dependency); - } $dependency->evaluateDependency(); } } @@ -79,10 +74,7 @@ class ChainedDependency extends Dependency */ public function getHasChanged() { - foreach ($this->dependencies as $i => $dependency) { - if (!$dependency instanceof Dependency) { - $this->dependencies[$i] = $dependency = \Yii::createObject($dependency); - } + foreach ($this->dependencies as $dependency) { if ($this->dependOnAll && $dependency->getHasChanged()) { return true; } elseif (!$this->dependOnAll && !$dependency->getHasChanged()) { diff --git a/framework/caching/DbDependency.php b/framework/caching/DbDependency.php index 4308dc1..cbe0ae1 100644 --- a/framework/caching/DbDependency.php +++ b/framework/caching/DbDependency.php @@ -28,23 +28,25 @@ class DbDependency extends Dependency public $db = 'db'; /** * @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. This property must be always set, otherwise - * an exception would be raised. + * Only the first row of the query result will be used. */ public $sql; /** * @var array the parameters (name=>value) to be bound to the SQL statement specified by [[sql]]. */ - public $params = array(); + public $params; /** - * Initializes the database dependency object. + * Constructor. + * @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 init() + public function __construct($sql, $params = array(), $config = array()) { - if ($this->sql === null) { - throw new InvalidConfigException('DbDependency::sql must be set.'); - } + $this->sql = $sql; + $this->params = $params; + parent::__construct($config); } /** diff --git a/framework/caching/ExpressionDependency.php b/framework/caching/ExpressionDependency.php index bf70291..e13c962 100644 --- a/framework/caching/ExpressionDependency.php +++ b/framework/caching/ExpressionDependency.php @@ -22,7 +22,18 @@ class ExpressionDependency extends Dependency /** * @var string the PHP expression whose result is used to determine the dependency. */ - public $expression = 'true'; + public $expression; + + /** + * Constructor. + * @param string $expression the PHP expression whose result is used to determine the dependency. + * @param array $config name-value pairs that will be used to initialize the object properties + */ + public function __construct($expression = 'true', $config = array()) + { + $this->expression = $expression; + parent::__construct($config); + } /** * Generates the data needed to determine if dependency has been changed. diff --git a/framework/caching/FileDependency.php b/framework/caching/FileDependency.php index 8d858ec..3797dde 100644 --- a/framework/caching/FileDependency.php +++ b/framework/caching/FileDependency.php @@ -7,8 +7,6 @@ namespace yii\caching; -use yii\base\InvalidConfigException; - /** * FileDependency represents a dependency based on a file's last modification time. * @@ -22,19 +20,19 @@ class FileDependency extends Dependency { /** * @var string the name of the file whose last modification time is used to - * check if the dependency has been changed. This property must be always set, - * otherwise an exception would be raised. + * check if the dependency has been changed. */ public $fileName; /** - * Initializes the database dependency object. + * Constructor. + * @param string $fileName name of the file whose change is to be checked. + * @param array $config name-value pairs that will be used to initialize the object properties */ - public function init() + public function __construct($fileName = null, $config = array()) { - if ($this->file === null) { - throw new InvalidConfigException('FileDependency::fileName must be set.'); - } + $this->fileName = $fileName; + parent::__construct($config); } /** From 0824e1c15d20c973bc2dbf6ea7a6f254f3755dd2 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Thu, 4 Apr 2013 01:14:40 +0400 Subject: [PATCH 028/104] Removed old reviews and code style (it's on github wiki now) --- docs/code_style.md | 328 ----------------------------------------- docs/full_2011_11_12.png | Bin 305633 -> 0 bytes docs/hierarchy_2011_11_12.png | Bin 16324 -> 0 bytes docs/review_2011_11_12_alex.md | 192 ------------------------ 4 files changed, 520 deletions(-) delete mode 100644 docs/code_style.md delete mode 100644 docs/full_2011_11_12.png delete mode 100644 docs/hierarchy_2011_11_12.png delete mode 100644 docs/review_2011_11_12_alex.md diff --git a/docs/code_style.md b/docs/code_style.md deleted file mode 100644 index 92a934b..0000000 --- a/docs/code_style.md +++ /dev/null @@ -1,328 +0,0 @@ -Yii2 code standard -================== - -This code standard is used for all the Yii2 core classes and can be applied to -your application in order to achieve consistency among your team. Also it will -help in case you want to opensource code. - -PHP file formatting -------------------- - -### General - -- Do not end file with `?>` if it contains PHP code only. -- Do not use ` 'Yii', - 'options' => array( - 'usePHP' => true, - ), -); -~~~ - -### Classes - -- Classes should be named using `CamelCase`. -- The brace should always be written on the line underneath the class name. -- Every class must have a documentation block that conforms to the PHPDoc. -- All code in a class must be indented with a single tab. -- There should be only one class in a single PHP file. -- All classes should be namespaced. -- Class name should match file name. Class namespace should match directory structure. - -~~~ -/** - * Documentation - */ -class MyClass extends \yii\Object implements MyInterface -{ - // code -} -~~~ - - -### Class members and variables - -- When declaring public class members specify `public` keyword explicitly. -- Variables should be declared at the top of the class before any method declarations. -- Private and protected variables should be named like `$_varName`. -- Public class members and standalone variables should be named using `$camelCase` - with first letter lowercase. -- Use descriptive names. Variables such as `$i` and `$j` are better not to be used. - -### Constants - -Both class level constants and global constants should be named in uppercase. Words -are separated by underscore. - -~~~ -class User { - const STATUS_ACTIVE = 1; - const STATUS_BANNED = 2; -} -~~~ - -It's preferable to define class level constants rather than global ones. - -### Functions and methods - -- Functions and methods should be named using `camelCase` with first letter lowercase. -- Name should be descriptive by itself indicating the purpose of the function. -- Class methods should always declare visibility using `private`, `protected` and - `public` modifiers. `var` is not allowed. -- Opening brace of a function should be on the line after the function declaration. - -~~~ -/** - * Documentation - */ -class Foo -{ - /** - * Documentation - */ - public function bar() - { - // code - return $value; - } -} -~~~ - -Use type hinting where possible: - -~~~ -public function __construct(CDbConnection $connection) -{ - $this->connection = $connection; -} -~~~ - -### Function and method calls - -~~~ -doIt(2, 3); - -doIt(array( - 'a' => 'b', -)); - -doIt('a', array( - 'a' => 'b', -)); -~~~ - -### Control statements - -- Control statement condition must have single space before and after parenthesis. -- Operators inside of parenthesis should be separated by spaces. -- Opening brace is on the same line. -- Closing brace is on a new line. -- Always use braces for single line statements. - -~~~ -if ($event === null) { - return new Event(); -} elseif ($event instanceof CoolEvent) { - return $event->instance(); -} else { - return null; -} - -// the following is NOT allowed: -if(!$model) - throw new Exception('test'); -~~~ - - -### Switch - -Use the following formatting for switch: - -~~~ -switch ($this->phpType) { - case 'string': - $a = (string)$value; - break; - case 'integer': - case 'int': - $a = (integer)$value; - break; - case 'boolean': - $a = (boolean)$value; - break; - default: - $a = null; -} -~~~ - -### Code documentation - -- Refer ot [phpDoc](http://phpdoc.org/) for documentation syntax. -- Code without documentation is not allowed. -- All class files must contain a "file-level" docblock at the top of each file - and a "class-level" docblock immediately above each class. -- There is no need to use `@return` if method does return nothing. - -#### File - -~~~ - - * @since 2.0 - */ -class Component extends \yii\base\Object -~~~ - - -#### Function / method - -~~~ -/** - * Returns the list of attached event handlers for an event. - * You may manipulate the returned [[Vector]] object by adding or removing handlers. - * For example, - * - * ~~~ - * $component->getEventHandlers($eventName)->insertAt(0, $eventHandler); - * ~~~ - * - * @param string $name the event name - * @return Vector list of attached event handlers for the event - * @throws Exception if the event is not defined - */ -public function getEventHandlers($name) -{ - if (!isset($this->_e[$name])) { - $this->_e[$name] = new Vector; - } - $this->ensureBehaviors(); - return $this->_e[$name]; -} -~~~ - -#### Comments - -- One-line comments should be started with `//` and not `#`. -- One-line comment should be on its own line. - -Yii application naming conventions ----------------------------------- - - - -Other library and framework standards -------------------------------------- - -It's good to be consistent with other frameworks and libraries whose components -will be possibly used with Yii2. That's why when there are no objective reasons -to use different style we should use one that's common among most of the popular -libraries and frameworks. - -That's not only about PHP but about JavaScript as well. Since we're using jQuery -a lot it's better to be consistent with its style as well. - -Application style consistency is much more important than consistency with other frameworks and libraries. - -- [Symfony 2](http://symfony.com/doc/current/contributing/code/standards.html) -- [Zend Framework 1](http://framework.zend.com/manual/en/coding-standard.coding-style.html) -- [Zend Framework 2](http://framework.zend.com/wiki/display/ZFDEV2/Coding+Standards) -- [Pear](http://pear.php.net/manual/en/standards.php) -- [jQuery](http://docs.jquery.com/JQuery_Core_Style_Guidelines) \ No newline at end of file diff --git a/docs/full_2011_11_12.png b/docs/full_2011_11_12.png deleted file mode 100644 index ef50fcb255bb0e34c042c6d0fa6c082188b03657..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 305633 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%0A1ct=p0Po?5{Ce!oQa*Aif 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

    F-e0+d?#qbWF-&t zf5`dMVRQ7?h|w!OJ&Np4X-M)?uYnn_w5&^XXRXe&Jf_kA3U+@}*1T(93#mg19;MoE z3=eIL%ad+#TfJ8`7%LAu{zg4E{IS9+J?ckjMRrY6Ezm51eZi6ujt54ec z>f?^b@h|wYdT;O1g`#KZGXY4IG8FY+=ETp)c+raY#MT;_(#_l}GxiqJ3vGBb_#ZUJ zQWOsV1*(8DL#-_6J$-3mY|Q*tl>cY808klR;LiuO{2VwzJBb}=NCg;%+#Vsgc9`om zg%6Bj71YIOCZ!cFmX0ME6Pq?DJpW4HOmdzU2&6R+J?}W?w*O>>K+e+wSkxUULEzRt zQ@Ax`!meDf$SgANM+T?4(ELGji2xdo$LB{IkX(-B`tP+5pR_e^$X*C30D#`!-d+*j z=Cm!A5^eye>pMCkTc(B%;+{}Ed*+w)t(%NDOLn!y{|!ym6&IkiRjb>j0J21^gSA+k zQY}koWxndw#`DkS_^k$v1;cbBPa3c@zny8dW)M5A9E?Bo<>rV!GN;{gt;iyG5H;`2 za2u!0HY3|>V2r)lk9WEYG$1UY$85kjd;jh@;PvN}L(Eq=&1eWstZ*C#t%f(t3Qq@> z5};*#ANZd5HfpHCJ<&~Ia^Ku|ud2)cyq1Ap;MD{eIbf;{Q^X4za8sBf!B)NT`jo^w|9+K2Fa#&7k;* zdb=6J5bGq4e>kC?>fIx}{-R~Owpah$7Pco{i*4b2yu5G<*kTfI%NxIEpfx7tmb|iZ zYp}VIk<%$4PCjUty~mv(fUimTk%GP+@v9yg00FZ3;dGiAru6~p7pDQrfmCGaizuBysxgNP*wM+fYtOZ_oR8FQfm@6(saXeSxs7#M zIR~327nxYAG~whvWO{V*8xuoE%AaHeBx&lc#ra~%OYuc7LNbN|=>*o%pp zN+0SL5K`Cu+44M@B6PJ2m`-m$`}(rNL-<ENK6doJDHSe z7(HT+w6d2-s8Uou-q3=DtzRAtu6PS!wzjKPsuzN}5nPI~82SZl5h$*h7&roZ&7&lj zv;H!$1bY6@nXH+Ch|?S}Ckb1rjV{|sF2|ht_XA&9`x-o#E@nuYT#vd!)dwDm$0u3LtO2R-ky# z$k8vDH0($3M|4fcLK~fiKnQ7C9{mxP-~iwcfLWZ~?h_6hgfdJGB@z?Eo@J3kFG_H6 z^Nr1Pec8SzX^Hj!wiqnZbtBo@%836UgaKI^Fy1mW0kPg#S3 zmLZ0&A)(|oUFWdUU8QM5Est11{Bf+~VMW{ywMXB>#$Z<{?QVyCu&(h$A;8CJ^z(~; z=2^Q*=K*T@{JPbG*1lgUuTRfp%-2ZL_4}1{a1$yW92!|7uhKNhVk*fEL;)0~in!}2 zTSHyFGgJq4kBxQq_jR+#PrSSDvre}Dk41HP`=MmHR1v;|FEcWp=WOtv>wi_uaUJi; zB?HC4L!PN zVrRY=-2f5sfU^S-JAh2O6%`!QQH)3&MJoaz?H0nM;dM@aY-7!Qw+i8X3%uO|;fo-G zkIT%o8CTapSOj0|#`d?m7HN(jRSjyk_5RS;UHwX%O~Pgi+Q-_=6INgHPcyu7kbQR# znP4oD7p@sHNsSVXO`2(40F|0;IkKq_e6SSTP*o`%h$2NomrZXxx7S)n0UrG4fQ;w9 z?sjVmD%0FUj;X9i0KgH1+±oEiln5N_^wpxiDBPFTM@IygTc0NnneMn@EUyy?z| zpD5I$dh=nW-H58pryD=YFIjkUv{9x3)gAj-3|WG&(V(Gtc|wA|%8$~$=_UC8x^8R)?I&sh=7PktgLBh4 ze%NlBZ|g~x6l}#qCr?l;TJk2=CxqkfKR@2G@AV%l5h;7m+WpUP5e?qeGy4;vQWycD ziRvLpVUYm-{{>HaalSG42%+UELXho@Ng)wt<~O8$T?O>oaE+Qd+1BM^q%|NN?qyA!g zxC8}l#KKniU&=3KJyzO5{gr}VQ!1RHDQNyv6<;dBUYZ>&somfL#VJRwL%5s2F z-E)!@4gg28!9g)N5U<1O{U5;C%T84fP*B`~U}9a}xAT*~AnlqM{2A&3E|bA$8yD}d zvKhSKEkFEV>Cil8G|iuh6r0{G%-61?Ymi&x`T;kBEjQLI);+Ci8Y=vc)OAiwVl=zB z(bhI9-fjg&x>j|{ZJMWC-Vv=71_P6=UKj&P_Wv)in#4|B1~kIvzg7D&+R@eqFG>PI zfx<#dAlT`x)7`(6e)kNlF|K~rER=RN&a}0a7NeLNC z8AsS#t>EEa+alOD{;*P`#Y#zhlH*VwUC&kcqQCdCt@SVFWg|zw@BpnqOOD_22=Mo9 z7wbA_kI~`!zoW#gQ}CC(*HI3=IJr1G1By9v^#uhMV5BL>;FPb+dqBt6(IM+;ZEXc( zP~rwY0jBV9E{CP;G@5(Y=B61qa)jW>f$H8rP!)4*dp){`wEqT>e@_&_0px&1i#=|O zPf$XJd0=+rrmX_E7N8}xUysi8n%kgk?e%@y2Degdye=IHi zC!G&YaO8YLuRW(eLjs-KGAc6b3af!pdnltfOFuIRD_^6Y186c)AM2IW8K2t&DWS!e z00WQ#x=17>1>XZ<#uVKZ*O+c}=`LK%E-&xP4kct9-h2cEoat$4!vb*g9a_HN4|2N8 zCz2H3V7vWmHnt?(6U8MZ#^x`(Y!(6+H-#>2pqfUP-ng0}gm@SjDe~D)`rHzr@U356 zUAscBR?fsovGFAes}W2G+t%li36#oCq-P2 z*tU|tj%Cm=gYwnVc#WPCndbieT)eihTcby4MMF&qF)9_B!?a!fN@ArFgMu1O$>C@J$6z({SF{re4c(*r8(@ z5{CI}5e76*(rdO#{YbjAFuVNYDv=I)LV$Y0$P8W6&jsefpuL4eaL$89P*CuEuoXW? zYX?dNHi#Yp12RpRuU2r`QRe6Yqhoabtn@{%4Pn#8SB1($Y`!n}H)W!)FB0qgU2G(s z2kzhRU{fxB?+cmN|F(;VJy4X1!9=e|^8xPoY04d~AKHclmd$uNX{g^1xD__HaF?-Q z1N4uM@27Yrhgz~~Xe}V&@-2MqQf=3YI==b5(0IR?gd6kc0iz_+!36ZyqQ)J<}DMwnO;Nj2q26_!?uHELfOr<(DZ~_#iVu1;Xx=p6qjio`{_pf@&)o z)4c3XNi-C#PU)>eb&``_NcwoL)t&6zoX7Pg zv!0}epXLp(2Rju}KWZZLBp~C*Go11G)vLiiYFbkx&4&q4Xx)02D>cqkdtit_mTbys ze5!p%q-WK!MNptbu^vL_G$UhRb73zSXO6ahmvWU(64aDcu^!UYUTg7XK#Ggh{`59j zaBm~<`}MQO0-ln;!{r~(8I&Sy@gg3Wtsg* zZ=lGBTM88Uo(d*A-JE{VutgHeh$2Sp;g49?*k~hh8i%?Am>KxnYU+3IBh8LiB!PxJ zdOvQKt@*H+1u5TWG(J;zuL*R*PM5_+IEx=&wRA$4Ybq(IJm`zni7&XGg9vopq`Y_= zqiO37w7Nl^r;3q{C1Yo~+jU6JF$c#!Npn!?iE0gbL=K;hNGQ$mveA^yl2$<-3OX%Y z5&Xy_lpJ_HV0hFpzY_WkQGsP)nrhpy_~{e*dR{hx92r3Dn7T9xW7D%hvDwoA85x#0zuWJVO+i%7cd0b% zH7Nr(bz1VPq(&GEWfLpV`gn4O7=8#_r(J@)PWzxDkrjf}p>4W0t+bqXXf`XC^Q zB`!4dF$qb}0iZF!#oW`-8^LVxLk6HFleGsFMGmsK&%DEcZ*p&bNL#&PoG@xz>hx_d0)FRtZb-!r_hu7OcYM!ZfT+vEVObVyp#hqA*cl zPY!j$0PDQgC3$I576)^%p`k@@PODp6Te}>K#Cav$p0Aey$xFe-`Qr+I3r1d8e}>3ed{3&+EQf)Czk*9Hslt0HvqVE|vt)^$^`kzo4KwuUipnF|F|9E!xePX8l{TE};$O?=x&8ENOy<|OKx!QS4MXJ#{G)UE0O+6+2fH~y>3bFcfqG6*%dpWNxf}#m# zq^`G6t>3$wTUws5o&@%AQeZYvU?DT{I`D0-{G7!!vY%F) ziqmU~Oq)iroGJ9!>dt=st(Bao-NO;u`w272Tlb0m3Qsf3o&<*F{$d+3F)>A+)Athf z0P)+*$T??V;<%N+p&(n@q`1Mm&h{?#ko4;;It^e*&ENqS%*zhUS3u`M8&J6RtTq1C2t}ES2ZwS* zUcGhvPjetr37cTWe+o4`s7{q+XBmD9-X?0wqa|+__`Q#h{zx?HG}KYiFCauZrh6OU zA5cWv*(rRR3%rpP<8`KJwllA@jkU<42yOuogsOt19)P2>KZ>h?onDlMT_J6`&y?YB zYYog4c<7$hXcsc<)H(;0x&MkQ|KU@nd@QQKKEU94GUW(@(K#8#3`jCix=2Yzf&qs- zU6F)L$D0!L!IcG-k?{l1U4YEJxr6W_Tn7w%u+0R5!=Jw6J-rrMgtPP_i>v8fRSU1= zJuM})dX7LouDl-5l(Jx{$z+)CI$9=`XZ^6|QK0~a)DW@UEhJuf(Af@pMW5dL0t0iE zYn?%<)ohll({`mBn~m)zd%2P{1`4QP8kVptXs$p5P)>O@^u0F{T*(o^@jk-)QF!#R z_9Hql5U?N-Sbc!rB76bfr|0oka^*Fwh{K?WO?48Io-SkSj!l{pDi^4j0*JFj0MaIQZXCjyvO5&l6z%V-QAfvnA5N86bpP#hEz zx=jR%`DmWLH2&axG?6~{7lE2#~+U$(gj(^u!?_}yJVRbLuNfZ5mp&9kH=&^od5 z*3^`a;52FY@3O9g#hwgEQ{RaM5PY^m_<25o{wH5^V6EV`@?v}BepnsgLK`Q4*cXemCguBByMuw-qFtbL{tkb2c=E z6euU#a}#;qCk;(V*aSkiAmpgW$G`wJAH(N(aCI!k0&tgIt7ADj!XeCj3jsLu6#)tH zC(&&LJdSH~ffH<-t;le3HS>|cu2`w4P9@janOM$$%jFr{nu5M4XNxLy1FV7Iwd` zO_<5#y-4Kc>*4Z6nMOHjyucPl1Feq{s2i21b_MczVO(Ua9GxC$V2_dB1`ml)IiiP`{@n>nK z%%{xC#m`#;swYl|-Qh4^Hx~YLy~89-M+-?eTfTjL(ZzMuT<8fp|J6{L7k1DAcN}Uv zy60(AcXzic;o{mrCg6Q-X=!8mbajF(;*y|y*GYUSnND0ewpx8lq@cG^x@(JQLg za_@-Ii`+z9nU(Up`Vj3lvsoxIZo8BZe&?;_X$m*eN5f4QsO#y*M+umg=s>(-Fjkv@ zYKRum3H9AyxBOvyw%>HWy&jTN7YS~2laRld-Re(|i7#RK2B9iI{^FFRe!TKE7%3^_ z?ib&JBqbek9A`F!5Vw&sKrE^%$ozfJZ~0Jc7i;9}@bcdD@$b|N8RAP*ox_RD3M6Wn zHABBAO}{ON4ULhz$K-iaa&JwIeo1V%JDB{`x&2gsun0`XeM6CQ&e##fU`yoN$ricf z+4=G1cQEXmL0M6ekjH^OPI#!|OPgEB)NE}nGV2o}K3AT7pNSdNQm1agS=DOIr=0J9 zxm|X4xR~Vzr5%JsMV0G79;iOAINYIrWjl2;BQn(dM%ij>ysU)IDbAkym3d(_BcwK% z%}}2^Q0fu`uZXXzbKn)Mc5hFGHz@?)n+Y2`no$EpzSe*YrP?YS8W&d$w6lR~fRV$x zQ^e@~>s^k<8u>(TBX)PtlcAR8%Uv03je^|7O6bwR#R>P-@Ktz#SLDzR?zYoGyZ6Dx z%(&KJJ78e->wSCh8GSpli;!=syYACh8sk^=1#(f)%__MpZbXE;1c}XnK)79A-fdiB-hO%CgXo{n#m79f% zjudKHo-}rCZ7ndL#3UsFW$uE+Q9!hmmnTC30P^Cy{c=e*J!p>c$ zQ`gAEBuwtBS?ZNg2QO;fFQ*1%ei-}E(9nM4lBx}F!5so;?53us;->=oC2uI^&u=}S z=!2n2PVdgo&qEy0ZkKGke4PE@eYnTEqGG6QzqkZclfbZrVmr&thTFkcOjWvJ^I=K- zeD@K(`k7+}l6M9Rj{4J8YpadBq++{uFdOcQn8`36crBiSZJ5QJ$F#q=xG;35_T(ug z=5YW^W8b$kxKSc*)~pkRh!7^dsi1C6Yh$hcc4-_2qEB#rile^=TU_*HSCWT#qTDUr@HlSUMN|QdjzOz_(pp3UB{DLSwr`%(kZ8hn%X%nBXg%VEo*yDEK&i6yM_^B! zk^7|$4Gl#^e2$<5*f}fPVlkM0}KFhd$r`u5f+IX)R7JavZfL$EW+a9i1VsHt0zY77&P zwqh6|l*+3gKi)E34<%#4aYtjN2PX>J`Gk1$Y}H6^ zPbPo5$JK-`@PXEmxHXpn<6`C%0|zzDqFm(5;tEIJb*F7GS57JVUG-+a>s$9#jT#3~ zrniOr7)j?W9Kw^iz42+&;B#~ArKd_lZ|v;tmw=>9Bj8#MZ9P34K4j=RY34;lreP@n z2bR{=oY-gjeeKNM3sqngnvu5vu@Y#-exm;^!+y2|yR~a#IL!W~%@ysooQDN7(tWg; zR@=!0-Ea0oq56ZpzZ*d;1y1wQ69$xZp&h01LS>?{5mV8jAQ<~pZnxMwPPfw^-|Sgm z&@nPjS6C*QQ6cRN$rX8?NBozy2gM}Cg?{FeB%T-g+>)wPjcrRxCF!0ahdl%&!H@&w z*m9@7TDjzTyf9I%hsM6cpstZS{;K40t29xPrVhCuup4DyHzjm7%(=Xm8|r${(b1UH zd#%UE4CR_`d#?)xLsdOeoTffWrlisG0>W^Je$s_ISG!85d5T-x@nd;d+0)GD7%&lU z*p_Kht$qGRXLe)1&BpLiqHfvi2}f1s?!(%Ol^QJNi|k#V!kV}2?_ejUDfA2q^W3g1 z<^dgh>ZY`g`+cG$W1eVAZ{OnFzbhgtsxL9J88r3j-9lO22r-ec0x~_AHEp6=y#|8z za?!^@!<*Ne;R8k_q9TGDQE6o6$a|wEXd`}?o`!L+iR4OFC*tzZMIzJOw_*OaTdfz@ zFY1Q}LnO9Iy5G!)LQ{Q2#$mtEJ-%~67)({dak-;=BX+Zae<{!PlZ7~@OSP# zVXM+S*~Q8>=4*ogLt|JSEa5LEEy!{_6iLjzTp=CoFHq^3|d z>Q1I}v$JU!7|Jr}pn?11d4+q`_f&<2z0@z3GTFl~++!y?4=ELH&N?r|9yZ*?bbE)_ z1eQ8<5IZz%1YPsPzVxT!66HU2}}%Zy1tDNNXN(5-;|C||MoYqe<>dn7iv*25kG23qHwK49`lO9zz} zF59VBM32;4OmRo5v7D%YdA>7LK=3;izbR{{*NWwJ&Ac?Q3CN=YY~Y^O zu-C(9XR|q~Xn>Ry6*0524xD==`>XEHS8ns zbq|88WZw;D)eb6j62+1;l@N{4lGNC>?JY}Qj1!saC1g!Yay`G0vnYRzz9;dXx1(+8 zh_p}OO^`enKXKsV^;Y4VFZO}N*uLQezF#y2V84#^|8P<27K)sKWVj{mEAqsfNiSroA66WrrIgOc~=4EUuBqJ&$ z_4-7)r9mDiJAzXiBQw)91oWpc8v6#>n%>jNDqDDD@-E8SFYbkPx+Oj)v*mN3x^mw@M_*-kQFp=m@4V!-t*yw&9G9NB>blRD<5N@lxN$L*OfIGdL(j)uhXrka!Lw7utfjGmn20x=D=AhRF|ae(Z?&)Li0 z7_>;O7vu+S5#B}kX&*H7Zc<6KATp?A`I$xlGJah4dJ`AjfU4Pi^iRK&wsgR^I zF}-GyM-LGw_MGO%THgn;|8@0nn2(p5jj@b*09Chr^^q-KA0IteV=0dE+KsEjkKqf_ zss$BKS$)+sPmm5~sN$kGPtTdx8*rwvMC8*d2c6wh%g$^xj=cBf&!|!Ma~J3lIgxH5 zI31gYD<5^H?)CA4(hK8B!zVLWo9TRVlziJa)ndtna$T#kq z0{a3h`pE1xqeG-9*>1x<)jZ&SZw^;m*!*qyZ^y)&hWC*8fpiZ-^Iy`vXsl@VTrynI zo9~X5#IH$YT$#>EX@}f=wYN~pn&<;Un-LlY-YjM#^K{13#YVSLY@`5b&q0H=3#glbvkmT!!tD!=>rp%(0XL|NrKg;OG@^`~7F ztGcIZwc=xR?-A;->pKH!`UdTfCg$c)Fwh6N0bvA1Ui~N8 zs{oXk9-1$DV%B>@F0MXjACaDU3^25eG1Sa?oZVFSs4xqEk&TlB%z8Y%a zM?R97zA~{{U0=WT|O!&lUAuB^>4C^CnfUi!cNhzI;fww9CPiGxu^ ziG%o{C%XeY!lANSdOu9}1Iv9^q>qQ*z6xLBcHCBXaXBy6@6aJ>jG~Y2J2xI09E^yH zYX3xN74h2bJgQ{6P}ytmDbo*7m&5EO1O3l$euGo;;sf}*@BaR~){@q8i?=0R&%tFY zAw7B(ZXYbV!+X}#M7@wIBLd^HUTJB5F+#58@TwrOY}^wzyP&1Pqr$x93fnA9Vwa!b8XHd&6CKvsk83E&B>3fYxC+!A7)CRyM0PrC*6Ab>E?7zZlE8=Bg zDF%d^H9dGTo|-DmM+;pBB{Wr!we;xjBmpKA8~NV`nhX;H>180<`<=QmQdm*5z%Qz^ z%_ktx0A|4R6@gSeE$tOfOKU5*5s3L*C^~{k1T>3uyz}}_Of9pA72m$i;c#_z)d)@o zu?IQ1aJigkh5@lps$i=tT@Mj4an!vt8gmh{$0HR*pz9w^jU!~3kV*Y_N5n_Wm8Ud} z2rqAI#&>cJt0)oC7%rQ>nCe|M*b$2dFQBG_dGIM*w6H%G7n$ssrl5)j zqzAA>{pUDR%pH;h!5(g}Njr76g{eUpD&)MjSZ7H_UQx}cNq^ZFe`iW#>(>l+-_x=Nqt+Ek4 z?Un9tv|Nir#pg8nAS1|$2wu2r$A@9NllwMb-tz^O-iIRXCd9H@F!$leP{{@l#4-+> z$}{rA3HJ#~HZ)XQgktU3c}U6@c)jNhBUH9Q_6|y->+3sN`9&E4>$g9Cu%iIeWC)=TZO01u zibpteoI5QNJzs~?OT-7^a&E*?wC2Vi&l0h}?03@COr#BYS8bPV5-$GzF0jlny34pC zNExkMu=4)i+tAFE6rrdg7krN8g(x0$KDxl#NNY&j*4D=VV?7X7~eSInON$={CP6prNVArq5 z&4+a#f+6aOuG>{$dZ@E~4fdMVguBtu(Fh5+CJN#rLo6yv*9PG0%(Y#y;7PGOeH5mR ze4mz_640;oWJyuLgb77et2%afi5hoRnyngQuP-8cW{*%2l~^WtQAZHX|B>{TEy6fl zrr!QuxnQ`y99vDpxmvkva>(tE90!O0OnB3ANl|htTXSn1Y0KOhQ`vJ#SEOt^Mr1*R$XK{rHaKJ&yI$rSrb-`#P_2 zj&qE08ES>8UHPi-HPeaoyKSrGX!3uxs+0~R9MLx}X9iVltm|rLzZ+D(I?_Yo$l;bx z5cZRsY1K%5Wgt~w_0cWjfB@y(v_;Bf){YWPfl1tOv0zVZUWc?}k5!j3U!cGL{yoo| zn(mCQ-wN&fQdk|}?PAsINGh~ELc7u!S)`Ox{-P^HX>)<8R2MosJJ;9kvsvsG!5;gK zcDK0xLcsFC>)JEHRmbt2UzWYwcNj&@K~SE3r3;u}kwZi?Tp_tlsEP`mbDWI;3IqV9 ztCaJ4iC#elXUz2Iny3T^S?x{dJ zScWSWU+HS2i6vxc;?Qy#I9#f2;1b7_9vOM%K+H@tWpBqBEDLVV)hx|MHkX}=f#C=H znR(Z!AyrM6fbZ73PoMY%w_I_Hv1!jo;6$JJ`9IUjI2>wZK}Uph9j9FsWWU)&ZLrXcjw>Q0lsJT_hsZzHhpuw3!j&*-|<)zz}HD@(H6 z+}wo?G6^;;2S=6h_UvvqdHVDzRDmHE6$n4}_n+^!nOe$Cu{Z24T7ssht;ND35jC{1 zZWA~mN7&&hIp%a!uP7bqKUqzR*lZ*fqcarY?s?ynJ@aG(!h)3e1Ox=hBbrEQoprd; zP2zISnSjP~=logX*{R>SK81Sh=kB zr;+X=lmR+nn%yMbeN1?*DOi3_NCnHfjYTTS9OIceBaf|J4V3wsGI#00qS=HsFZsqt z^2*Wk@wDBzBM%Y z5_`+LB-<*4;ONn6iuvL~A=cyw;7_2;np{oHKZTuGTU0|jNYvju$PfmwZl2tfV`_Yb zj`i^QA>>BlYBVSx#sXr79*i7gTN0WdffjOn1jsb=PqS91>PZcaMOs7!0lqzfnSpAn zxBg>~r;_7np2r3TU=YqKQ9m5bUfA%KFUjgJ?IS}*7@ygVE-y?eUv266n(47PLCdNT zk5k-979wgd))@ZW>*TdXef}lL7H>ttC8kMw2AL_%M?b3BTwLr^C)BIFDSn-yiBN@o z*SXhuoe59yT~!Rs=OflfZZhw#4_M1J{`)Ei9#qxC@!usDTGLOCEnCj3T>im|yBBLV zSLi@6rdGPXaI5`(*{WB$I@kPz?+2;>-7I zcI4}WF>O!jBDL$D`n@7#Et}tS`NeBHSAE0n)tI`atu_kFA$z2NMB-Yh_~8d6AjzGU zQE8J6z;#5{evTCEC8(u!#?VeFywYEI)om|TFmr^lG#0KMfat+QE%M$3bgNu5=g_mN zxXmW{b}k&lW;j{sYv7*6?3!#$dP0)tDzBc;!>dmBlWN~=;ZvWBs~zcDy^iWpba`sK zBq-CRjq9a=fmw?e=4&qW4Q6E9cud*)B8{8I&E5-8+>p@I|E{T%tB*?Jn~BGs6O4wN z9LS>w2L_&+n=1i$WCHN`v=@6qnMCa;oze^3{%4;~Kr<)ijX8SfU}n(D=f5^^o=5}d z!YFwhW5l|H9d)++~58u;s2|9L=!S<2C+UPYa-Cj z;l;6Bfh4H;JEqxVm zM`;LU!Sc^q^-0^APybE0{gJ!h>V>%l zsmVPBC~cOfdgCpe&PJA0-^UJ+J|<`k8&6IRTGVZ$lMFcbk!}1jT%mqLQnKHe?27?p z5k$1+Z?-viD$DOVKE#^8PDV~7&I;xVF-Wkl9~JriaBnq=9dOzkH3e1C7U8SIy}r{; z^wAq{^Gr4RLUG>g{AU#!l_p9ZeoUj&s8WK&P>~vhrXcCx?ohr9NApFmHnUKNHL+Dz zR?4H*sPLVmjm|AwJzofs(;|hyxEZn zV%~g?xY*s&@~O(K0Z#Y*B=%I1WW^rr&;$cV5O&e za6gq-q?z#LSre9e{`4SikOT#&ldLPvKH?1PZQKf~%;@W9M;LcGUve z)b;{1eLcM~xEqp>k1u6I8EfR_1Mhk7^UFYXD=G#HKFi>K!h*?zoEXfPbyvv)T1nI0 z-D;S}IkHA&^Y@_vI$&+G3I`HQD=oSl6=;{1mOzM@hXNvPh&F}Z<`}0u{qGl79u(fqn;Z-ydyGFI@&X|+zyKm zMKq4-ukKCB`cAAdA8oQvq>l+HT=zB$mq%|f*P{WS!~>V$)?WY`p}I%ZDE%3Ud`_p? z>D*O;5B~Ft^AIJ@app^nO^@g`Lo?N)dq1PO@d^q%UY0U5wKsoT)~zyfP?Vk1NbYa0 zE*#7VUQdDaWau&^&Y3Jus-o~aO6+X(^^;kiT38GZ4DeKnXAEwQjI`d+0lhV-+QNFXVM3^P(&mEof@0TO9Ha0mIzRAUJG!r*?@>^OsEMPx_}6Iy z3%&fv$OkZwosl6BVv3Cs7pNkov)<2%6-=7y14#U4WQ;~bHPB_V{5*iUC5jv61eQ~r z_)YT@wJ?g$a@eq6(;H);=3M-b&zfdYyZm1^74_*k5r<1>3!-1&rs3p0e{+7?yST_^ zagvT=sWn6ncEfGG5R(p~z{axIa2e<2(;R$!Orcg2ACphm-6Tbthx>556h&M8Q>HfC zQ{?9w1UGpU>JuBk_0}52C5f_PzGqB4Uv$c4_>`d16W6N2? z-#3QtiO8Y(4EL8B_g^!6%HE;t2WFV@K8=b;Xv+&r)sDeLsLv1rA;q7k0WsB z(}gYm&(OL-oNzK+CBe^cGx{Z#j*c$L-AGe2=H0u~X0Gm`Et74j%DEtUfig!WguZ&yAeMBuJ>iv;z&@+S&f+ALz2Y?ra}k}v0t zJ?49r+tP2~Ml3d{TR`nU5ti{>A@o9&qJfx=hK9xkB}y6^uLq$E-^lQtL0Zwm{rSEj zoN4Edm41@jEW|;3kwSdMBhvbYhDNl?o_WzTuVs@O&E&I}(v{MC^R?|BB%MjcDeA5%4txqA!eDD`~Cd zU<6AwI>Tr2*pX9z$GN}14OkzMz>eipxJmUjobT<8feG|^=QN>`nLz%C)gd=pBaBFq z*}V^cLDB%bgBB3j4UbvuFv3?wQP5&O+bHEF?nFya6~1osb2KbcB7yzFSJaZ&w{lbZ z6+PyrqKzIpoeFhswl{ZaWN zdnC2~;vR9YIU;R-nA6deMF8=XTKmcHAWHZpTMo+Mg;#{6+{`S&$ zBadXXlxURqR6bFbkg(xe@9mMUu8j7@Yu1@Jj4S%ZfT)7Waxiseii`@^S-dvDi+Jp1 zTg@fcu^djXV(XV&9v@!ahF0j!(}xj#t}@gFUV=ImlxLu02IW#_;h534oci_b zygWgjN}Yy3WAdF>;Wky`!}`wit4H?{Va zMd^?fndd*siZg;W-pgzPwt(>P?#|WHQ3Z0onq4>AD244+?4c2;g~T{1DOMJitj;(^ zEf04YIQu9uk+&Hj6eq+dB4gaLrO5#7FE%!X*gL%IA!ReSjDyAXH%4Ff^Qc-}+=8H0 zMKD;5l?+U}H26W2ta|&YJ@3<%J%T&mXK-7Xl%=0Ncg_*6)~gNp@`6R8h|hdHNNJ<; zlu`9^YqG-P!osanujUu#4;rZLFM3g=ftj8*J`|7W=jMOY`)4*kpMB7CD!g@7p?nzu zR-0-w1=S8bJiNt;Z``4zrJ62BjSd_*AiHrnV3@OFFd-$9MB94~={7gpw-D15*go`A zb|=a6Tg|2)_Ihty1z@y2%Wgr{JV!V>K3-Q{ z{h5Yh3;lrI}Wk&M$Xf_Wx|XaQH!#6|YdX2T3-E zTii}pYv(!t-;2Tmv?Ma%VTQwP0L*IXdAQ4Kk`W#~(0c5g%7!b(Oz|40UW2TCZASkc z@yrV>GrAnhfVG;gHK$OREUucyMIN2e_$E!ft{k)hatHS zIK`m=2($+LkntWa!UK}?D#JGMkyc&7k~oVt^>)irbna8H>}$PmZf*`Ym&4>`95oc+ zNkLhKzEoI57ZTVn^G)Z@kxZSPt))8UH{VmTP@{}3Xn4!S!kTNSI>jrmoMjhiHh8nq z#-=1xh{ay9h>ir#mHr7@Mp^KW*@(an6 z8oeF&As!#PTFO?CtUxw_r?%FsSi`OdVu5`fFeWM(bQ*84_X~Y=ZvQX1m;!g+!^B54 zA8yZvBEa`CkXY4KQpuNuiHRx4s7L7W@wRVR+V@t(KMv0f1+=w{w?kVtuX}mx{q8f) zzthVCA}=345P1R9pObT`DlJ~hIR6quuQXft*cr4czuptd(vU&crXtFSj%#j8L1N9$ z=YJh$It}UFrDn5kANajGHgx7~wX;!54tx*yJ3I(W$P#+HjJ>z?y*gYUZ=qIOEt)>N zv#Tr2-=A);Z&DIdHcT)@?WLrp71Z|@4Xm(CwKv{o$bv~3`FyqX3d!_Bw5K*$ekP2l zE}in@^fm|%2r&QtVfO}6V6>tY8X)MS^TS1+McXj-i>Io3wZduo?S+AbgtfUFzi-cO zF5BQU*qg?wKwIv?4%LUoX6wFu-(5oeygvDv!W+0I3d#{;4P3u*C~|vpPzd^+n1<4Z z*!+A&wp^GTWeD)_B+Tqv*J9CrsV3*T`Qc}>G~MYYtjA0zKF&&TRJ^zYJccNf|9jH* z^aFLjW%B{JbWIUoF_6V&V!HY^CQSwIVY|cKdig96RBi&RLXu>*l?PMwb#!>F=MvVK zx4z@oe^#N_m6g}>LuzFP^i);Xx_VwM1*QibvyRQHW2Hjzc&gXW1n8)FN=V=tDcEj==U)im(0USf>}nj$5|z`Rjzarm_h z5SO}A*SrB`&fpNy?;;&7au$nG+edI76_VW729lWT4sHiG7x4;=U$g|3z1>vr=5mN` zn9S8u=sh5j-XUkOl*uSLDa*g=V4}^B-?RJ!KKb-zXv+=sF(gWwXIk=_YaN3s8oyIT zA9)hrd;7;9_wUPKFxef&V+S&lf59w%;;h*J2g~;_Db3$dFZeHvDExi1?c3jdHKiJ{|v#ZnWlg=Y|X=L5xqWt zJ;7mSLBUrLYvF|o_6hHxdmGNp8m7C(*904}FNOc9gD`If5itP=%E-N-i3MrzYPmyv`{>-<<@ zOtUPCx_tV!oE)lIB*e^B|JuL@kdysvp%aQ@N&oN>P5UAY?kXA#RBvc(6t-J_DzInf zW9!K;Julzq`Bm;eXGJfGf6x@vr!WC5* zK#Pkz$F2N{wxA{(T z0x=MuykbA31DMDw&Mtz-AZLhpj7k{>Pm;Io=0?0z9Kdn{M3?5Vt#x^^wJx_0&;zxr z_nHnf8`Nha`{{Q1ixFv>{~h*G{_hBnn<)$Xmfxa5r2&IUPEPjSzXXy3lB#G-Aba5M zkn!A-5;glxAo$K)xrR2)jlZ12+WwS z6^4_HSs`s4E-wL9<$hugRb~PwC#RS}osLD9a&3LJ$nH)P=(*uPU&sC`FOZ^zZtfRC zQIM^h%4;y}mh^YsM2JelkLO+c6)+633$~pb!A5bf;XfD81 z&OlDH|K%04M@Pu-EnOtDLm(73-3p~&ataYhZGF_BTEd<{uWF#j7x9FDK34+0PX!3f zsUi*FGGA<(YNO!d+fU5d+8qmaeec^t6nId%<6~pB_4WRaF!{vC$0wB7p4&C(9UXl1 z2QqAyNAuWcw`(yrQMx#Y9YwLcUfSTjJT?U8+ZqNAg~yjJ3Rydq@$)u5jFZHV$= z-5+zK&0|v*hhmV^|H>!&3X}rM3EVFxi2)HpjV7}2ASle;C|<${S29Nm5>cydN@Hs( z@^A2qp{@wOn6||)dP`_-NeA>|qDyWf_?4NRedl7j961E}i(* z1;e`*AD=wt0O=LJH2x@r-K+yKnyDr}iPC_VN|M_nLA^-|plQ{Xs=7h`MI0sy$OiT+ zt>1MbDoRSfcu+1RqndN3PCD1O6SL$jVU?TaMI!_-`P`6`nNgDY)~-M zP_~;+d{Aq@=^zi%aJa|qORS_MSU{w@p+$zw^)VvyeTiK_CpN6}kyK%gt?F%@u0|d^ zbx9(0MyCD?bTE@Kkf!l;e(e*qg$s)$p~q?yZsgqDViSSYhpwQSsy;-&&Z4|e($5!f zYbNkoK`0$;+zJo0%eSh>{y@!9s55A|Pr;pFfG^JLT zg>yksv3)!|xdg|2o^q-Z!E)Sew9|m)Vvdx$3@P@#H-uo}mB}sECPQZlIzBbH7XiBg|z8Nohx&{ zwO#myOjADeZH`K}xJ5KF_&##HV*4NXE4xSM34~H}I3hUS#2o*hfVBUgPNO9fblW!F z!Cx%1Sn6|tCFE5!K(MjJB&%YE<~7{>cj=S^fSF9K>#Y}VJ;PaV>TS1Q#nyOWMY>Ip zx_EX#m>|O4ONe}J_*2-alRYY4w(TeKt=sfA)?Ru!m`&WTU`aYOA6!erU%9hxOm1u0Qj(s=3WQ-$l zzB)A-P_HdOvARyFb5rf=>Ej7WNg?JIndd#YQE-2RqHIvcr-uhiy(I>|rfUFs=2n9- z&ME~!1%^FEfJ_;bUPOVkzkbVG7nljMxq;pEZd;n_xAu1Oo<)go>{^VkAMZ3T|3e%{ zPaF3yC(A4o?71wtijs=74GyyU zu2GNV2=KOo85V}iFyTmJzar5Ou+vtSgc!i8v3a3*5!&MiHK(@SUIE zcaMMD>2QEo{x@W_Q2ZhNL`y5yJqS+YO2ZL^|L3^L&Qy6Pz$YdA6sOdJtxl$Mqg%&oSQV>3M>XcSL7{g(Fjt#soRj-DC zG_TsIkA8F{wm-Au&gU&m^`KuQK@_&cobox;|%iZ^y`VoMp(V@YK6~1!(!eQUtvK(Zxxg|gJKI@-twLP>6 z%o13bZYn-3b5@_<^yJaf(zdk-v~DS5c@J4=zj8khRt_Ld6vhVFga)Q{c~4f64M=%qrL8SJ^+JAZs5!NFCM zhSRaU_dW+hKMJK2qU(yWJ#@5;gdRS_uOE$IXnOiM85NcPqx`tMxX1M>w8M8`u4^47 z?TRrT05Y*a20ofq6smCYi#?t5C%1xwp^Z&M z*xS{lXq<0cl){3Ny1ClY_hB(Hw4!Rn0_AECrGCPgTvh6KP#GkYl*MLd%6RB(ab@ma zVA-^o>`ZjJI<}&a5*(8Uk^y`gA(kVMHniYzctTw3TCr@tY7#SK~qK#<}!QI4W^>!T{3gtEu0+NO$D=RDEF~|o8 z|uJ^ifpr=Je5f8@KPYsWK>PM<0B7R2I@``6DsnqOQ59R$c$vJeIG zn1{PN5fM?$twa0v=vhF~%PbwKm^M>UV@%s3+H_^?lNe>!nwFQXSH8JvVKdMO(%N6? z*~Lr2jSXTi>%+YvrGuCFy&SDk3gcaPlNsrv0$f)^QuA*GE@h$Hg344p1wd5T2G(!o z$0v^-pmU0kjiuswa#@&gmTd0#dBX_8a9%bO2nKk=+M2QCN2+dnNA4Zcpe66Aunmw- zKlAtE%5)W`Y@V`?(ieDbA2yZ?s{~^NN=UpK>ySdex;BK>d9-MEkHP#s(}m4D3zqYCn8RQoF$#*amKLXO?KB~n5+X~) z$=wbkQ^fQ8`M1wF=)rEZoF$!=&V2OzuhrzAqNqcP17|&|VyIyAmhaT#|4Qq(IduFh zUquM%9?x`ln(zN=M*7#0?Jr5+FL~Afm_8ffsR+eKT69yVNG|C_P9V zd~nE|+w{+-aH^-E-63OgdL)c~!|JD|?Oe@>F#Tj_(9a?30gPvLYo`3l6pig_vOXTO zwye~Mf1lEfU8emRXxEYz(D=^fP2(L7Yb8v_4!FBuCGckXJFYCkncFXx>hp-SAhI-<W?qUn0nwo+^9rBb@Cj% z&EOXr%G~IW3|TL3Ih(RSgBTd1nnFU9;KFrX2L|m+Y4-983Q(wO9w7bj%Q_531}`5; zi^yf8w($-;Eg~TlFuSEXLG`<&fCeymUjq2wp;r&$ydl)h)Un0j7&$3!yh}xME|N|C zegkw!sbe;owyrkTjSyqKnZl!|$)NlEFN6W;DsFYJoWEd_jO(lTNK?%FxvQ0WRrO9y zcz2sLzx@g(DvCy#D&krVTDn2^(@FZ4=48x#%5p$O%H={}i|4a7M zrl^?xhW{qD<-}BIK4CqXkBoie4qUll5F8Zr8SnNG1-j#ksbYAtw&W%>_P^p=%o0sc zPL42L(nrt-isSTSjaLMxuNznBvd>IzeXJmqK$A;rdI(0xYWvnPw1E3xua#*jQ5bVV zd+lXTNo8k0<*Q%2nH1@{wLnDBg>sW($yFi?RsEt95DRezR&UZ`-AL$IpYU%_7Ye!= zaiw+>zPV{;VZ3tplt8H3ZJLs!v%-d}4Gv#iu;s4muxDCrzUQ>hSD-hCU zverV+YYqDo*sICTg|yD=HkNHbp-oaV>pUzZEbQ>)2?wFeMc5-(YGG^!ssW}Wz#XgU zM@bt)79W+6?L88>R%YT9@tBQ*MgH+G{l}bP$XTBB@Rf1tK$=;#TFf9Q>PDvP zJ2h0wFYG>vv?tNOJQH)`T32E{i_$)6v$`vO(GkLSYRBQ#s2e}{L&pt$+CNG^U=AH^ z9Gjd(vJ7@h%E|dt#LO)&f|9lRo%Z5nmzsWnZNy+$-(knAr!E=18LpK^aVH5YsjC|~ zuo^>t0p&Go>w;zNhgKd4&YFtt<$KYLt-b>KWJE;VaYyVGEZdN%Uftru>BqH)0GlA5 zuzrvXZn9v0mrs8`89kS$i99vl%PTH6(AMGMQ-1Cb&9BOH`hCtIU#65d24m>j(Y#@dHtknz({|3gOVTuSYMbU^A+83BDx%ee+{^<@NPK=Kr;Mqf*`_ zpyw_q=|Nch@QbXrcHGfCV96lDVGs)wPV$h7)n`;T?dclM%iUUa{J(q?(j>O`&fiBQ zP*z0Xo5+6VsuDhZdY*#(hSGQI?K2rX0oid$hd{!vo|as4Ta9T0B*JDb_T}l zLB7q zONFfgEXDYp}gS?d+Es8VmEE(D)v<5Cr9nqI{*!`1b$qI}a= z_pEheWp!F*IArC_)3xhEJU#Kn87>igSnpQ_IlG$5E!qcAPncbo^`j3B0 z*$PPr`Iw}Nm}FEU^sPusM;gsq&fU%Zb6oq6Ob|yOm``-fcbXy==bOj$GipeOu}02(+Nj$ntnu}YFlH~d^>1*U#*Y>88Z4zhp-^t_$N<3ci+AA zy*v})lMxf}O|Ix*T8Sion9m_lH^SP_TvTZzFBR?0Izi5XYK(J!h~4{x&)(GIqG74( za`fij@c- z7nZU94J%}YQ^E$zIK~dPrQ!su^xSf$GSc3TS{XH3n5Im`f1E0sWH`(6d%So-*7&Bs zQsqD*F^))qhZ6t5OGwhu(~R*7s@pr;;g}!$=74mBP^y!Gdnw7GEghG;uJ%RKeb=+^ z3XOqhfJtc?_Ih6qZN*>^ce6-3Znq^>Y5uhcKajwId+x)ktF){B5ICE6zg9FtQpp_r z28C^BtPTC}`gsV)cUi$#7Kg&FRg*q&jzYuJ5ozBofAGLI>dzN-*Lv{7Y__$xCH1J}0 zty|a+C%#F^jUKG<+zZ-SIqr)YQK8qrf#hxZE@yl88pE%ehaf+aUk@IoE&~}`-#R)d z3qAE+F~j$NGPBh!{|SEnsWxpnbIY8pd0IwJF7^QXmizd^Oh>UzE;fX&XPE2R4)R;O zHFX2F(m0=>psJmuF*I0f6BR?x!PU}v7XohuH&$ntnFkBskP7_e)xuzlT(>@t>ga*k zFklDw;^Fg@7)K+XiW{eoKY8`AYIik8J=ZWS*l5=EiwPO*Yso-1fAjGSqbN>EYYM8LKMCf5_D);Du&up*K6O_wt?o8VL znIO$uJ#d+#A4ZjrjFP};zqbD+;g+YtqUL@iVkhkpJyMaguwX7XAaxNLCe0!pZ#!{* z(Eiif#V%^HXF)qYIKwFI&HNOS6dXjB}A{x&VEk>&c34OMIEDesxvNjq)yHq%9W(=ZO#BYNd<9Id`F}9mYTdAo??< z|2zHJ;SIfy?Xivlw(2QrLtnps9cxW4?|m}o{6NgR&wAmII&aMQ`1lCHQB$N>Gy55K ziLGWP=kd33bvHM+XcwhMvgtXVn{z?b| zs6}wdxvG0(Qk>O1#p^vfY2!4;wOB~pLrSCViofHrQ`D!Cd?3_6Tn zF}cDZ4}|8*ikm|qXu`$Dk(17c?B2mE`O+8BcSFV7t*7}lkT&rF9D{ciO>H6{ikeAN z?)a_%bn5#LHuu;QQx$i0b*s(13@jZPojkY^mo@%G9k&uzx^AUAN|4&H7>!n_)m~Nu z947=$4~^6B0V}q($t#YGj@C3bVsmMzsns&IJvwGU=e?ljSxCfZug6YGObpPv=7nt! zcp3ZSO$H>+5qexhZdB+65x-`;kOVXs&M+GR61B}2tb@*G;|c2VI2UNdL0z7*G!$wy zZWkXH2cdVLBKnX~>)p+uAopVJcNh!?H#OA*ZolG6_hb`FvjNDK0iCwhwFqR3OzeN= zN3ubrP^5QsBBYRv`PhVUpuSn%hQK4Q454Btx)bxy(pDwUPiR#IK)AC>0c4%jF2k)m z5VKWFf$f+1npeHEPF=(IN~wcNX2lj;2_%{9A9@^JD6vU<+T)d4;$K)|o57fc(Sbr1etV|qL*$Pg%O99=A0$`* zNy)K56xjb>4Lf9ZFaOp`YnuZT`~F|AJpTvoHLLbGiuMbH>|;}v>3imjDUIbyFyGDu zJkt$tqQ^hEeA#CA>DUV43Su;cNZ}A$KQjIQMs$8+qIsuxSIH-7%%ly zeK9?OgK$-cu~lC-DV5kO2u-t#O-M+9h+klAGK7p7(;$NX2@i-DqAE{#ZuDIc*Gto{ zLmo^zqz{biTTOSVjf|m4UwuX>aTI=h75$ zt!X`4v^f^rNR=)$RWRKKUQqS-1wB)O(>=Ux67#d3%4Jhu)BE94YtPBGa6y|FW$)aJ zAj<$!Deu>J)?U4KG9xZM-5Bh?QBhHdc^tW(ZTu8*hp_)Gv^!LyYqI&_Z(B-)ovl8A z%qlI7EkDMMlzllKK4bqfx8=MLGCckZK6e+tylHM19tarZor#U#FfdFpXbHf4E&vj7M3X#Z;z6jzKX`aKNm&)naHLl2H?Ct zL)NQ^_+axdUO=i_x>vbny4>sexUo1FFd{}_T^yknt zZANejq@4ahdO1#qvF9G9l;gZj?pvoK+$hKWTX{$Wa*szKDd*P>@Z z1nl2jV}UYC-pEQxxp~VwI5<(L8HBCf>qMHf&o0<|vo<$3gT-qy0;y|if>yOdc0xsI zw)gL!W@k47gl3qR&QWq8izWs6a=eYmy zbtJ!5F~SMU2sX`0CJ&~VCAL|w5aVn=@GY)kUlx!G+sr>Rcym=+#_rsEY}KJgq1USE zXG^fOdnx70=|$q<_nGZI%7W+}#mdoC$|P2l=jWa;?74iy&B2w~6XkS+#ox(#%`Jbe zy1E($9bDSgF9fnWzwmDTO1LPYF6(m&ov)B`5_JtkW=}j;k-qZRT?pm~?KnlFk`u3s zj4k_8Yez%$Dd*2eM(V%Wh@E^iKSqmF|e{K7--c=L5`( z6??WBk{!|A#8zmgd>+8ix zq>Yh#I`hK&;CIiiT||z;{W>JWJ%^sCN`uG|^KCnB1{qT|XzL5vlMQ@fd?=KEczB8VgpwkM#dJihUDRrG zCmZ{MOlP45J19#aCT8K~Cm}40b3F!QD#*hEQjP)eFdSX)*;tEML$ZEn57}`#QAIqv zBrVDXt8nkxnVMDp-9xp037l_5uHS!=bkO%U@oRdYtPI*srT&vSaL2brwpC7-c#f2> zz?~wddgHh%iv#f+_FQ~V1Upt9%vpb39(gfe$bA+~<5N%4n~;>BztsLH0ti(@J-rJS z!H-_l-gxNBOm-A18ol9L20Xa2?K|stSCG}DL!8G;NWfU?b7R-!C3LAaA%sZt-5xpz zki&?6oiS~HBInyj)87ReXGF`HO#9Et?q>@q&5054CSr;@w`ocf-}wAiV=XUDd#s^{ zx@RM2mYqsCiSI;$Ri@?!hC5AFaG^H+%dqF_eDMLC$&KA2?-1ks`Cr* z=>v0>4cEW|{c2%uo`4M#aQj&L~d8k*<7)obym>kGE!Qje8TLsy)Q{Ug&0ZbECPd*KTlyw%v;o#RMR^>p zHw=!R?>oH2h8f$$oss3M3JP`}R@bWNMt9evIL3_Yk`Iy5&;(X5{OA=?6z1j4M?Msh z&5v*ZilqJ(y`#g50z8eGpXe(5N0L5>O=AQ6=zXq=`E818zmL!W#g1jd#Fwry@mUuZ z78eiTu{$(1cb!<5(rxc8X3 z&CR|rxwLM8ZY5_F+yk|>GaW+!Pdz0;*p7hJ=&8q(%;LZck&%&&;BdbCt#qt5NCWN9 z+N8-l8D#{jXhKZLpkM^fY`2=o-nC___#b@L09;$qEJ}=|dFkWyEE_4oW>3s@zmb9QT-CmHxZrE)ynI=GX^%Dj8xZR@4XbFw7 z9nF0BVZQ)@DZVPEE{&DjN%SoG<7pXViyaO@&lX+Yl{N82&>11w;rS>aCYIxq$FYGAEd8&V~QsO6{68`j)WsbCcI{D5( z(jNN0eh#LjzW1S z5e5CEDYHpC#B-)=RLxy$Nju4jkv5oRp?=ze<4sbk^C905dWCH-2478IiKxQ<<<|;{ zo{YTG=giy~HqGN7nuMSJ8*mp0f`y+SIUkIXg?=i5{7o?qGRjk@d>^ZD-So86ig|xE z`T)MEgz@!Z6{(Hn^oV?z3_Qqf9r;)Jb zqh(CPyq5-Ddr%5di@u_svRsZ2C&!&Sx_J+qM#5sHV8MR(1RRz zL>tocKRn-?W~Tuk1(r3;VM!MfMY;mzEsCCWaRX3PeR7Z z?qVA)Da8ChrD!_cS8KwtA$CXFasVlwZX;Jf?-*Ra=# zO(pe*HX1?yIG2BAbH&anUIkx?^I1~Fzq@*_q~fJ{akxTcy)PsoJ;GwEUnnd+fGs0s z%-+gLH|!E)c!F*-If7>y38q^6mMe6uWZ`}PqWlz|ymIw&IXUwx7w+K)YnvfvBFiU3 zKkEC%HM#82a0w=Cbbn?APy{BL6o4mM&}Jbe1 zbCo!Uq%VbX;m$7uFy3Wa|eOJP+6EoW1RkaVYh$0br5( zdb8EIKINo-Yu5n*?-tfSPe@|GHfxWP=tJyvv)^X3wJI5vJ`5wcH(at|eS5L*5t{nV zu_8}KLFGwul*1UUeX-ChOT%$>kE4$O;kHUK-iPAKNMRo+a+x0%c)90-8j%7>i9}41u*jo-c{zON;;8wn02D!MbgsM*GehQHEP4U$8E?u&muGI?-F5%wGMqR{sTx zpI>>o2Bs%KV-)4gDkv)}-??)lP=wbWwXz79Z{(-~bPIzYr)Cc60GoqV=id4<)uD*B z5keey)+_n_Y6VXji4IpRzMzWbbKwy)34d)B5aUnfy%}H=?EtV+uU_^fe)c7M6$R7O z43!eiE7W{?c+X>8&>)DqsSvUn%RP_0tpg2Li2{zH_4+49pIOlqN9LW^fbhuS zTkd59NxN+2>2nrs54m@oCisI z#sr`GN#NKQ-go%GOW+4xs7ESF;3jhwIT4Y)4#_oJEK$RQK8cS;aQ_69NfrWpe29jB z>RrULY&v}iW4u&_ieiQ@r7k@?#DV@p`gvZI6SH=TO8y0AW*k5#WAd*-QW;9oq)B!3 z6=G6{>vI1LC+aTH&A| z0$7jLdImfuUtUNos##qgg&-Dx)N*#sRcHnbXupB(+*&oKL@iAZ8mZOP^C!1Rr2`~fuX)dQ zgGdXlRy@XIbB++Ko^OhQa7?D90b%@tPnXsed*lYZ?Dg0{HJ^E~YcuUBoW$iZVXu`P zbusU3E6?a(e)RZsoeaMc!`A95LXE0Pf4phVujgR*CJb&?C0ihK;l#1|&JfWcup0w*O@QqLw20|=ZyQ=VK>B7{`wMF-=W88}|&=4Xl%6R$78 zR1`+r0TyWzoa`_NKzU1udfrx7XD0xTG(PPLw}1dkXfWx0_E6=?o>FzG-ReMjevTCJ z96o!*q|78LEiDcHd1fafEJ#KGwEt9=LT*w}>ct*q(bh2$Edp&2S07L;JLEN=38W@b zismRBF_$nfGz{2($YN*p=+BKkXg6NVymZ{NJcN~~i5NiY5DT=811)47QtworjVA=G zxjj@N7#VTAXE=Y}I?`3~txiB?9tkF)EAjgC{++4=_8#2xQGWHO>TB;~`smxL?@tvD z-&kDxI<}IqDElsJzzURXje|*PX|Ir5A%t_!5pwC#=!%zk1cqPVKY)8Rh9c**Ofi#_hU9@YzxY%Q z(haw#QpPU4q>2v>e{po~L6J4?mfEh8eKAV9m2y=t$e-ISc%M?>d7c;cljys zgm!OY*L!vG8QI8Z5_oGB0YSTgVE3SNIiv^beTwn=2yrwo%DzK%V#>6%aj^1X-gig) zT0YmMV<_*cxR3gHL8B}G6IK42JcRND`rW&Csr5^d^8n>*LdB$BR|NIV=n~bvNpRMx znimFtkwN6^G&uA)9=TjMrYouZtQPRzKQQo7#ciT~hszsrSOK)OuDgTuix8rb9YP0y z}wiLquTZ;82A`)x86Th%Crn-pORF`;?v)n1?PI8W_wWi{YC2=>M?y z)^S;N>(;Pcs3-=4gdj)?0s?}B(xKAb(k(4*0n*YXpoBCxAsx3$mk0tkNGT0Bp>*?I z3sm$u=RNP<=X=k8=g<9nHo}@~t~tjwu5pdgtf!+h#;*YEyM33_dCU_q9Dv%j>D_(z zE5emQ3wY1k%bw_nvr0+I%34FHqtXqViQssbFLx~JUJosUKA(2q+N3dKltm}&3B4Lq zZu*;yj7)p_-@|DnZ&~6zqUUz zcC^-!^d3WEyWTgk@y-ZG$@PY|o2#DMaYgfHC%1jLABoEC*PolbnPI(l?L}A^43XD^ zI-T`CVwefflS;sBEuGX2#+6`G2IaX<r1EIAoDBmh#opfgB6 z{~m?04t*#{{C{>3eO(UB-5=uARkD?eRaZrHw&XdgqHvI}%XTa$fv-&!)SM7&ImY?+ zy1%x1{-BRjx#jE*+}6`%;t#q!pv01Q5Y8|>aovW<$Qk`c@_<@6lqoao?P`)V!)Y?% z5*a()sjmT95pkDKMs<%_sNgpEVVBG=HL~jA2wEQmwvGc~li^A;y0|h91g$Q$E^IMv zk_QO!P2wKfefQwijGk%+Stjo@O|2JV?-LkFe|@<2%XbC50R)E}RJI}}Jl|+2U)9eL z&+F($DUR;%D4Icok>qk$+F_mjDDCq*Vr)tg;wT=5k4_pl+X9|!WOT$c)rs}guZN_ zaYx*Rxks?&pi%fSoD667lcl)DxErmWMVu1hpbS0#-D5<%p9Z4AXuOVXV&50Qeo~@T zj(}T@z&Y0_Zt=lTJHCzyGVK%{eN{Ua0ubQLmUUtUsuNhQ5!aW%FyB;{v&r@40LBEp z`beJwxd)5BN9p|I?}Qd$Sl*D)@jwk+f#{Z+r}PI{O;n7lJAIHVrO?c%kE8a^F4D{-Tw|_sOX4 z(R2R&g8oOy>i>!z-*%i_>wsgZ#)cKRwPEq|L$A|^Y${q|wg>Dr{}arOAu=9v=)RRa zy-DTja+UykpTqOtse;%yt(VF)oE=8wR!%b#hi+?O4k7>?c}DQ%$=zL~;*Z%vnFYP8 z+7&}%BJ(AV{#&fG%hM08w~`W1#%#WwDxsU1k4;tXH0(G>CttY{4qXip_j|9bwyRqP zAozLw%2{NI84lGQZxGM}p7`ODue@772gnuse)k1{8eS_fKT#^f&SAUMf<`-D=UFtE zB$cuAcvHSgziO{PAB3CX*UwWp?;S;z8uK>t6O-eOKS*r$?~z8bSnY`CPH}bh;35nr z*Z1z+p)i!?^7oW2s)fovL2Ux*TZAgYRdMeIQK(2@XlQ73H1UxG=j)8)G!i5f7tJp{ zZ-l+@;`<@v&FDQ=!-4dc*%uW`%-m8>HE!+R?=Q89jZBGVu+Mo)x5nP{CRLZaIO#uF zonepPkBKiM6+(Gr_%kS?9WT$Wl69Gw?I!{ous>*Q_V2MqK90XwY}a)5g0dlLaIs#- z-X#K>y%K2lNs;BIy1H?oGlwB|IbPVGArK>g7KzPz`P_AglKCm%`!ff&0N)cC2g(c$ z7)*@xzxwEKD^Cce@|yO*oa^sh3dac4TM@-`1vN|Psde!h|I~X2jQZ%iwq|{e75vLA z)8~sWyE>!Z7Zbg_VIjcwdCADYptinVInPKAglb$wa3TXcz@>_IaRzHS!mrduYfy=3 zUV~gl4Jj!t|3{N2h#Hvb>+9;W-);>YJFsu!t5IkNQOUUQHO6X-yg%SEBtroS&Zt_b zss5;Ja$D5P&AkDkoH;I_(+AExr`q`1SmCA!g|wvPC6B=?Qyb-=)} z;^9$3dN?5OK<$egYHv7bu71StRp6yhS@vXQ?p&KLYIWw#U*(zLK1Y4kz7NcZ4#_h=JnB&a;D~uK zyGF+7U84V^p;8I%gw5}6|oKeaJcRFti)r{jk^REZqKRpZ% z>{$*AMst#@IYqj5b_G5sbYEv>JplEum8^*>+DRd(h{t$drh#B(`xApAL4og?Gxa&@ z^~+hH{-heYasIxire5bfnu4Vh%7D|^+9sFgWp1jIlCvNMsxVSQ(O4>JWyK`aR&9NC z5TB&xwFo{VeeQ|>Dbn$Kwe=ab$W!GS!LA^C|?Qqbkx_%2S4$&`iz0$6YC*rggAyr9neHh8_C0;FTBxcb8YKr?r3Te0{{{jDM%E$x} zk~sKz`C?t;JY|L4JmCrex^;qv?qnIT>DKS#l!mdX{o8D2>M&1R<&mu7@zU)*Qo~rU#2i!a=_%YA!SFXk(uA(uc_`cXoR?4 z`ritr0}WI_Y&{|1rlQ0`PiRk<$P|nauAOz_vx?h+zi=V} z3xcOek(0t{7<(k$7SsTMgLAwOWJg94ZXxRhC+`61`^GFo^}wQ^=Yq-Bp%1Fs zd$>PfY~)gk9Plcut>Rh+^NF@m9c4388AHR^DqT#5qO6-{-!6UxZSntI&5;$Xet5yN z7DQfiylsG{A*D#4w-m;1sn2>1Cc>rzK0FOK;r0_4vp#zQ4U~*%ve#k}rhnjg!>`S5 z&+0ZebKph?MN(}QYt=bNnA7|Yv%OGt={LTBwjr$4eDOJN*!^p}yC0HXBO~<6R)j9i zR)6y>pp8$Wi#M0Q+eYG7y86T?BUI5vJOvt+$;shc7T}qhiB5n{Wp#{2=|?1v&EH;V z0REz295i%QVBny>xp`({LTo-HE$Tv|q$1nz?F9#nJnh+{#t8N8duQwSz4I@##$(>9 zBNdtdxc&h%9t;I94M@(}#&>bjs-S?sb$oaA|KfVX4!kMc)42Nkm;YCU?*9mbygl5% zm;oXIf2>iaTxdfWeJ#II9o9W@$Kn!ETl_=&f#T?>UC4hd*}EaJxlYXt{n3W;ZxUC` z*T<(F(W~=~X|tErIq9`_>j8L1m;LXCS}OTd`j63LeF(2w441_`{J zo!lFS-F6CnLj zkFOdSpZu1FsgGT{pP6Awa5Cwh9$o$cillS#WqU%C1hz-g?%V&qC$H;wOoL42`@P6B>x?Pz5g)T9lKf%RaNm_uyEI>s zP*}lx3q{S5abfCU78fN&Ro=Xp>g21Jg~cJms=)<+MI!t=qK^@-MhoW)B|zS7oq$o2 zSm(tqP&>SNb7f9-b2J(%>TT^u^U7Cm{p+;NaS&iaP|gSrO?WtHhoOM);Xy&4F&L@_ zNX`I4Ma?8Y|8|^#oLoD!<72HG3RQgu+x#>_Q9!p73x)6zlctFa(#OoR}oEaSPYjTd40BWAcg2lqbBcsiaL>E^zZdq7Z zfUW13z8|W3++w?K|X1nL)C|&b?V5J+rJHOxQ;STt$9Ku{~{spvA#(S2$Xc^1q1Jj$En&eJ1 zhkl)L5Y^kSrJ*w$pKPKkFMkCG&gq;r@DI|p{|W7dw$Rj$;+Vw`CRutLQ)7p%-`>bH zjTl_8Q`2@WaJ|{gHP@Ivm7BY>2!O%lDtC@fjqY+eOqC)M z6pL!7r`Q($xM#$#*7p}G$QlB1?KU^soOXmj{JUKlMeHH+6{&ep)xZ*3F9AF>!;>zUhFA*-3X zivH$oTh*9{Q-Z>$wS4Ae7WW9BZ>tO{>Z+_1xIx4~Ln9R*0)ivWYd6@~u9Ot&g%h1I z1wBz~jhVxVa+mWcUFX6f8@EmTSN?-U&BLNp`U{4YD+z@R8s#qm9XG#^z@($J-qrr` z6~_z1iKG%WH#jl+;^m71&&XMa)~}@Ed+xZLl|waxt&wu56%@)TMLcs~7enfj|Uyr2QG2Lay3LbQvWQ?3uTLOZcf z2IUHJxc=TQ5ReNR%&EgOcU-%=Hq`mYt%TE?OUz0afEU0|pI&m5Shw?ImEI@?1AHiE zlO$q8-DHyPbx}U@ld6NWaMBTXBIJo*Q$ho6d`v*VY1TQ)!B@6;PB z<1ml&c>iIYyGW_={Pa$#(f&xJ*Rdjdk~3WTvn$*Sa;a7-N@FBa3l}wcIUN5I zzKJVs_+U|^gHcT+%pfnQ4()4;%+Js7=}{fM)@?^9#b7d6?aR;44|TOPJMDFpJvjBk zjBc*pX1{myz)#88_V*efyf5W|=)ztRJmvPYk0ti7$9ugLIjIf%lFNozp>C+Qcv|-Ku2_K#Hejvd3Fa7pdGVy^>lHIa~53Dy@GnnqE_SRc6)T{iF zxc9$m_5WvTb!q3wJb3k`rK;zL9AUcB_2?%=dNXt1uNz7sT_dR}!N$V~rz9?m1fpIF z-EE}oOtb8tX}jy!>pVX*L*nbtO}JLm*84gI#pCVLN3b!(pEg*4%djyp;+e_;?*4Ou zKt%xAp#n;37_S5q*x(oMlWMMy0Z{}qB2-td=qQzs4iBg3-U7pJ@tf;j4{p%EW`vFd zq&=QGLLZ2!q_Rt_nS$%^`3g|_r#yaA`h4e#9Uz)Dd1LwG*ZF>Pp~IQNj=VnVxGJP-SuRULLks*l zT~3E2fhd7FRGis#KJOr5$@Z%7<*lNMB&2p4hYIn+vQ#mUW0!M zaS|JUqKOkkvT%s-^FRG3U#~D+r@CbG46Ddj>=Izvib&kc6A~&<7wA5{b?Mc~VVk{~ zVFKHGtq?@jbR2>Vco}RaouB&q&6|c2!44%I>`oe6tKj7mwLh0|5XIT5VVVJRXzq_D zyUYi)^z-Ux$Ia1Retpws=8P-v8#Lyv?Vh>HDBBjI7B)U=Y?Z#9_mCf5Yr)XzfkVrPzomd?-5*S@Q1X$IYp2HK_Uc)Gg7cw$|hs8a3@#_qaUI8AcQ zZRqUIgyd?Mhld9yT`4{vu0k&!Y3e_#5+y|#;wP%?vokWHy=^BZ;02fh_V$~^#BnA` zCjP|9*1kT57CA(~5)N^%+jRh_4IHMhM=;sXtT)JqRY67LLhI{x*{neiHsh?M?Nad` zjQGy}Qmup{_a%9lX?K1^(qihd>vUrD1rvM?8P-y~DOmMIi6)$mG-aZLjH`ZnTJgrt zzJm)ETQ&~(3uD|D#y4tnu&fP))Of%Bf=Ep2 z899iRyXyA@Ul{XVs?(t3V>rrx4fVMsxhAs6V&Ffi(3m-Grg&G*EQS6^5j14r}yK(y>Hgj^<%G_ZRMlsf|_aPR3m{GBJW+`TQhR8T!)gGL4 z!DhNWnhH8dvZGEwZzE5J#0dZaAsOuo5~kkahA>e1(&OB;oqQ1xP}bMeON3MVLLc$P zHADy!#GBqZccjBPURz`)dt zAh+MvpLYc^f+g`3#XY%P*1q<*{d~^mgX&{jjl~zT`eIg&9VB_l;WW3rJumcoh!QMc zQy0yC@&C1jLeXZ3s=VA18lKGi0;j;pntV-}WhwJTY<>CcWlV2V_AOu|ps+EKR8yN` zRsT843Eu~+J^0C?T-}*sM{po09r->$$-m9qKl=pCJw|ez<=bTomHZy?KYLm03c?lC zBy1zJh&@9NGJfyX?9db1(3fu-wRLb=74PRZ+G&TDu1%MKbKTg?OxQ3a06H%ZDPJ5& zrv8Cqc>A_ha3f(vef5u%BYViV5t9T$TIjp3VCU~z)VU$acm@UrkP0v%gBE>)bQSs$IZ z7G6zPHLG)9y7-V-hI7cfbmWdRbM|p;dPM6x1R!H^5K!<;gOo@2Q=Mn`-0khV;>Pu# z^nXU}F#LWj1{IdC>g*6YtcDy459RP96TMeOo66>6e|;qL#M+`35h$2B_Kx73m9VH= zoH*|WWIyQAGBNa2fzxg;2SAaSolyAUTNYOe+yr2jUh>a#;lG{*r9&O<0E^b~QSqhn zv^3)Y-XCASd_hdBwk0udHIa++2%4vD%KQ8^v*yD)FNP7qJB(8>7pfW5<*sI;n!&XS zTE>B!Vk63D1h_x`F^H%R;+#9I-~JYjAURnMyVCKakgyYT?EoQjq^bFC#jBHU>gk_lOGImi-wY`oG#4(tk}mBQ|c>0k0fH6#_&`C%eHc zxL+<6nE7Y@!)FTag4bF2och~+aH@ws{f|w9(e2sRf3a!c&}ziz-12rz}iRK^yDwEzN+^ zOFRJ`RAy8wVtT#a@{d4{V)^35S36f9cG*QFaNQ8+(ec{%3!F`4b&LMmuA z!A|<$9s(Wd`PK{(21Z88FOrCkAA&%pGwc)rPuZ_u|DF`miV*)N(U4#OqdAC_0#f!K zLN{4(_Q|qpM&A0MEUqSeM7h>2t{PjhSKbmb zMU*{Gh&K30li8M0za!}X?@WWHL1GNJg$S|388Q0+&UR4y%|6&zU4WFRRK|?job<3C z-sL}q-Fq=G2{@SN2tduj!2xoZwFh03>~eE+b@7#~zI`~D=Sw8-Zeok+{a3^yTQ&pJ8Bfg0o+lth<_!dhcQJj^a~JDMEGtusOEW>8pS%Wz9X-xyb1Wl0{rnfq z>8sjD$5#b8V**GD^nKSCbBcyHH`e6hrgvsRXCt-@LE*zvf6jqh7^8oe4%)Js5l9sS zIHHd5$+*=#XPSw?gqujeNgwS<=2l z%I1=f*au{5^Cg4j$MC|e>CfEEYCNIXWhKm!+mpA0FvXo(MnntEa zp1-qru-5+Bl@?R+d;b*!bdntBw2I?>TQJ|H;2)anZVeR$!8ic(+L=6%>HNXJuQuLp zS|!?uWzER?B|p5QsD=3}BsOf`?oNMsSFcl1LTPL9hSJQ6Orj&MGJB+>1j(k0U*6zsr7?Pl>yP?jDQ;5lwHqC6LqCE(DW3G9hq;@CtU0%oPyStlkJ% z-AH@};Pl&f6-miY9V6?7s~ic1L#AGG!eEgu=?pI)J!RoqcNd;Cp)2zelt@# z*4)y<2QVn^9FP=QLzGvlLek0cYtZ~F1xAa6v4m%Y$KzbfJU3#yo*kw)4cacSQZtJo_*qBmnni+m8O82VuO93+AB@@m`<`wi=H|5I3B!_qovOCQj048ycMw=S}qy)A&- zo9VOEY`qDjnQw8davA?|FH&3DFZJ4ij1D*y#s=u#I$WMmF8oHl^c}^N{RhauXTTb# zpyjRLFI?W>CW_YjiRw2W1FEh+m^!KX&WfyE(VGFFH-w&`c&dVZ-0P|$dhEjKYz5WEfJy$XqFfg(FZ%-k^suB0B@!=%T2g114v zI)HkxI0rjfHP8JNYo9%U7)vQAs&uc86IOImW)d<@yIoZev?Vtt zsS-|63E6RL@!NdeOhlK(^~Vh5P73b*-ARPMh3*vBAO-gi+VL-(k3++t`G@t&f2C&s z`(K4K`d{^WiQN&3|20KhFQ(2;Z~9x}gDtAb6(pL{5{n4s*6v5mYJIi)$JN{#LTJ|+|TS%Y5%XY`lPXJxAw5)O2f99m{ z<&Bp2)8i7|ym-fWJ@RSo9UBxofnkm z=RS^6kt!3Toga+jvrEV}y^fS@raf`|IDFftkbDI(AbntWiCz4q4%!R$YptI@BS;Qs zgr+o5(a4z|oE}VvlE4ef*J6+ZG=e+u{~3M5QfVnyo{sWm4nX2Egnmx+{>eFof!X_jcLXqL1B?#3FmrWnr63iH~Sbqz!IOP~BBCaMY}VKTumVn!9|hQC&NZrg5+jTuNu1^D>vNYTae^k8+FGnIY{ zcVzV0!x8#1Eu_Gp%1b4KiVA#Z2c*IDfq+C!yU7+c<1h2``(t*Kiw=Kfo=^l02eD@k{FVZm0rvsu8mS6 z`=Tp@cE(XL5A>@aU|tv;mS%av!52z!bm5Z8&jHr$xyQNkteo{ujd3L2(vh;eR-FHu zxL$~0@%5yIFZaN;Evj3!Sz}<9w`kg~TWGPXzj>+2>VE})3&b}mvGCgS%gTCZ?^##BGmNZ*SHcCk&%-Y3_`S>i z9b~@%k!N55!%*aB4CK3o64*Q(KU7m)t>cTeP8~D=d5!XGsj5w6di1%F+Pn0*B6SIU z(0MR-$lyB1xJ7^=-UBelB03os9GrX_6fk*`+k+|nh0t=ku$e$6g{!prLsf&V;4B`N zB0070`o2p@vCIb3rcEWaRcSsS(cdel_lDasEsjff8athwPS&4CJ@72^tCl)&;csNx~iRt)XSG(BK2Tr&E)vF{pxs9 zJAbVHH^pnX)Xp{pDG(W#tSrP4>}++~drhAL!q>qku>@3RK~xf7aQ~F&i)S|N8B^4* ze!xPpTBGx8dq}_Jc4UKm;@CYDdk_PIfdIhtAJIRTECF^7I_83ms%^llt@l?mVp`B2 zC>J>ih2_2?B>g|X448WY*S7`3?|B}?=mH1ctS@{Wq#qT(#VXV4vi-$Q0 zFL~6sBix%q!^0V>ML7WML%MwZ{J=0rzx_DDhvd(ueWgmn<{<25fEv&R9KLc;tU=T; z?G)e(R8$k0=b!-`*t0TgB=U%4P}1*JET0COFuUahG}-DBr+>;^(u4mVPaPkt=>dOm zhp{<&BAk)E!-+wW4ecCFttc25!$61Fo?sEmmOBHftaI-P5ZgwN+1|~s zK1x(hjTg+^1!BV|kNnzg2rmMb=bJ=3AJ)U)9~S44^GfqMHM;Z#?aTj#QxCY zA;SeA%C}$|y^!gVZqEsZQQ-bF>9KPX%KkhF6$qkTzj?k8vIDj(M;eIvvd2oS+UJjg zASYeF&HHdu*}6E+0`kURxyotN|A#|>6YN#)wXVQVZWbUcmmf#QM`Fj6Jw13j2*g3o|;?Mbiv#pIxLhKJ2PyPfwP*|i3f)X{n zD%-DG4}U>YEXZ^KTTfp`OQ!rBCa+<&8w{{`Xy&p`OuXgz4rfQ?Jo!IeH> z(@FQJ!uCrfc2P)I40v01WD~h|<|$|qi2vVrQ6z!{V}jOmTd}O@h|ir%W&ecF$8W&c zMS zpmcx32AypaHIm24$q9uBXdn`~I61*xtVM3tUdUd?dh@H(-b){;q7_IFTnMcU9h#jG zrISx$)b6Tm!B>vVL+bvE&6vCv~5NO+!Y=XsNoMxwI&Op>Er zzZd9#_IKDCSb06DV>5L)f7Z@S4Q)HnduEaS3DtSn(R;wdo13F%2iOaO@%dH1%w`$aO$zC$8x2E*UA?-rj7)PZuel;-bZ!{b zk7={ix)sIHq$lwgHqHtLfAE!yj|jr(v-68J#>f1KILLeTplwX()N1Ffb+bK-_)XE% z5F5yJY4cN4qhn`Z0^=WdJ&!3zBvm^Y@a% zt_&}t0jY{QM>PqJ;(mnf!>~z~*%_#+PFYFGjR6b^cE-U~g~1iyBnM6pp)D#FH|(ZQ%sVQ(YBQz~*=kI0v^P&K zl2Vt2@y{rTvTi8DdV^H&~}D+73mi>P)Al2gC%Fch+)X+=xBQ*-@8ZRp^7TKC~Iq&(2_PVGwaLM_kWG` zZaWus^N!VFI&3O&N&k!jCP$He)$h7kzvE!_i7vl31sr96Qv;(;8z`Z57~#SNE-Rq# z$!ot&)k|x$D^9aU+S-?+d2eX!d0w*YQF#BJrPTeq%3u&yP(&crdPw)yt1D5-G_+!c zmHhU>oR4q4%ew0vs2yxAXeEZDM0><>`P8;Ymy6SdO+su~pWET3Rg_v@$cADB5%GFq zo%;G^-Q~~kSB*-hnzAs8y3BE=Vo_fWtj;eusx`nsvWEB-p2N5#Js?*T6Z2ZJE7eh3 z;C#a+*7ZI0&YR;ivTPsv_cRvjc;>Zl7!j*=W->}A8BJenB2K3dtGJC0%OcC9c1FF{ zG&7oaapcfHGgox!=1oA#uk!Nb%A!v1JQwQca}7kk#WQ@Gvkn6t(&)rQ!MjpSGkj_V zFhIb<^7-2XA`VL(TU&Y0@7x(#<6a}d=W}FFEq*7fJP5CTWBJ-7B+{@9D1~2`m@y~_ zk*7IhcQ^_=C3rYQh4^U_ZLEg&!rR0CXj2jLw&uX&7>N@>SLQxvX{BZvLl1=``eRB8 zP0Cyj8N(Y9#Vhblo7$k2g{fSNN3encoN`tkhSQo0*i>-1fS zA8|&kZeP1#0>dUm*LdWUtjhWDyl?n>l;MyF}VMJpOfXzYY=OY z@CgD783MxKoNISHE2_J2j6b*>Lo~pAoz($NJZ^%=cKl5=PJ$eWVr&zHTE^Djuk5?! zL^&kn#za+A`SNX?c{MCrD>mi!n=wz1iDILs3$5o^by6-(e&c_R121y*$LkeB?0n_M z&5wf;+-QYEyi&y~!Mrc!lwDKTi@0 z4MoN9QjrnniD?+KNOZ2d)qvl|$c{>kT|R#NIFf514l@8FHqJa6z_w4Sk>ssa8)IQ` ze%7i=@#M~XZ*2kJF>wsk(LCdP=VU%uciwC7(TcpOINq5dpVr0!W9j)#U}UUBT~;QW znUUe!2VSJA?r(%<3iA3#o?EJ4i^UnbPQeOOYwh=9K9as<+*7Hr&aVn9K%IRh!)g6T z4NzGZ2?zpa8ykIizMf^<5bzZz=5~-pHvETE1A#G8_ZsWHMX6gppKGGI`p?x#`@Uf9 z|1#sK`ifht{kJ=0N2hvGPnL^t|#&$ykASnS=i=N2!| zaYtV{rP4-D>*6nV#mviEa$|So#`X(mWwzQ)UX4%@^fEVvtg?~I%#IUStV;Au!8XHi z=NQ;)O!yP**W39M+Z~T&9HncQ<#c^ezUP$i4_pfxRH$~LdO+fnEDv0=Z81iTM?hva zt#{WmxX7bUau z|2@^51HFuU;L0Hvm3=NwR(DXb`uAQW!zk|E&!F^V8h;P0QUJxZ%7dD96&|PXVW-Xy z^25)=a{l^pIEm7SE=2y%{>KVxjA9biw0}{z8C#n=iZ;Ym`bARCi{=y9i(&M)mvA1< z9egAL?}3iP{mQSCv6WR`L!zJ{QEPQEcGL;3fLKPK{+P0bB#$r``_V}zdr$w9*bBQq zj(1^z9F}4BkFyo|7nU}7i}f~wUQ<2R-->ive84qy@u=@2W6!uttBqDdU-rv}bU4{y z77)zi+-Hp>6%-Vdl?k`Tr`<=-sCQZF>r=U^8RNq#(^1kdFdsT^f~;x3&x(fgsZ+Kb zq73gWPyTt2!&5pkUT?g|{c>$7s3;<(D8;prmndp$z-%nH@6%M8*Jy!1-|%|7h>mpW@DK=${ii! zxZb>y!WheaL2Y?WvbOsSVJYE+p&So4YJK|c%m%;o)z#{&=fyi`#qWCGk0BRSY8khN zY!wP?X6kOcBGvWB8B(y%m1kTATm2&(#U0q-)$O4?O_qrX8p0C=`E9#V$WayWPlRm1 zkcZC-;iv=30{r&i^8>kVs4Dwv8b$9d*WVYsL3y=9?rYo4_x&+-&prHokb;Np$w#SjUJnB=!<;*(;`wV*p(MNU+h1z<+}POI*QY+`6&HG*EKW*oddx~UPt{Rf*tp(#P8q)v1Y5G+SB7<(C7sdfOg8qw=RLL zo-}D1Pe(_G*_XF8tgLw%8F$gDSgwUKO74sAR`$JP*YDd#&*_?h-qu<>$EVc!LFtno z{GP(k8^E@<$WnI&E94rIaB=Lv4+sB*ca*bX*@%0l&^W-G7IItd z3ye7dg~Zn=mYFo%eaFS`i(4`c{qRJPd-@et)`j(zxmj8DXwZo~;i~7eslH$9w2czE zCk!r`oQ!XDYHM$QOE`D0-6qlRPU7+IHi?NwV?$KrMbXw1^Sg)QH&nLExZ1-9sI4z@ zDqa2|buI7xqb93(rQLan(^2`|eaPWHa%mz&(mpr`kE1dKhI@m8A!%n}5l>nvs)d4* zlBS_hN=sjV6p}mgN^j-)Y2`{SCZmnxrp9M0D=NX!&b2MyaVV8q5fPj*Bo(0c#I?{i(N6CC~YD|4?gwe`1{(PcGnQJTflipLXHPrJzIq&p5k6M&D1$vYD zH_t4Qay@(idl}BliE*cg4<7>0RX?DR*T$$8!J_f@RM?8%52-5;zu?W?%j3+cfr&w= zRYF2T35705b$GRjR8R@EHWk<(zxxMr3NM*zp~64UGc$!A%0XKwuB6X&c<^R-_e-xb z;>N6x5T>Cn>!63NOdO1G2 zDv#$u9iDO8T1brWD*JdC(MgX@{9|$>>U0jz#ry3iex7%a%s)n&4F@l=?#tI>oF@m7>#gP9n#&xS;Dio_3{WGM zO_dh+WGQV39cP3YO(R|fBJN6E{P=ETr4c{q-UNrX~_;sBZHrxjT&#VJ}XS{4u0%*Uo4I(J<+wjMPeo2+8V4CkpM2v$DMaJxG! z;>{h3_BA)ns{`8)a&gf;(_;5;p8Neb^ON+$knPKST~U}vWV;dm^5q>dvDJw*R9n(W zBYq28J9M-gsgYQm$=Y*JE_Ira%^obx@uk{qoS>zG!uSo#M|M zC$-?Dn6MR+a0vChG_^AP)0eU~ zVIy0k_aFs01sNHGZrSLCa&lNz;@^8tsHpmw(@l%r|Cl)Kjy{QQO8-}3AfepaBi!7obVf9OBPt7=Eb6!MOkm2^oumZi#Wir$W?=i^~d zH~&8&+6objqBD1IU#R_-5(d#eC`d) zunU+gfMI}tVQFhDazVm9=0MDb@oZPcj~C_?*T0}2B&~^J+yWeGEE#fbZEO;umQ6^w zP>;I3T`n80>*rpKY1Y?m0JZ7*59HS%*eUEYcWcOAq{6yE^KjwztP?($4Z0!9vF(q@7 zaw=Rx(1?N7lt>qLfs@FOQo({V8mil)5lx$Rj5*F>4ORWk4ps1%Elk@*^j=mY03>yb zKIq8~56NCPtSfv}Do}TRfG5czrQlWbXE2$&rJ#VJHK8~vfYSf#UTJdwvU1qfk!>S+ zi7|$h^PW7$MU&2Tv}3`Ts*hXCb@00eV^OfJ{`a9@2kX#~A{?y2IWEOo)FrB!;7&a> zq!ZP2MPkEc3y|fo;SLx1dKN%!lhwPiw6i;4l1CcAB&%+c^otRQyEdK+hI!nA?yGJP z3Pa=3o3jfmyL?U1g@xhh*o9vJqUmq^$Yru5qJVtzr=oUHg#8J!%x6wcOs4X&NrMk= zw2Af_87J`VYcVF^Kl76$Lojq%s`a-Pe6lTpE#HgzGiWRGSN)$?FFJ}2XRTSzpj`9{ zwr-7=5jmj6078T55ybSoc)~R@JWLZH)BhJ>^kxCTsF)$ouj^e!&U)giPdPH8{Z>wA z1wss}Yie|F8r@#ZY{xyY4+Ia;$b^pj%gD%MCAvz|YHC+6&aZ8JQ6Cu{)fL=y>7d); zCcX@(0P`@O4%c>TYisa>u#EX%FVnl%Ohr)}9y5X=%?F^o9ocJQBhn7z+*`jiBojaXS!) zVcxAG*I7Fl%W!a~7c@$Y=+?5CIXIUsg36YK>%S*GU1+!Mj?zM9*ZbTmv!wNxmM8Zp z)M2J%*~W$EPXaEyjo`x5e5$KT)q!c@OUBd&5-`mQV^WAW;FQ{he0p(s)TSA7z=XT6 zlp{D%124rffo?x?{NCLmQ7*KqTwiVE;U~5_6#MNE{wh6R#fd#l1W)?c`j+lNcJdVa zq!3W?37>j;N+B&Si$650F3?>MJ7>M)7?ftJwQyaU?D1Hh&+DVV84D25`aC>;#eMbW z7t=(!`pVw-BMy!&w}<6xzNXZVVidWj-Vd5z3_KD>inzi<8wQFb;&o#>_Y4?)jgQ@L z1Waezh4QL%`LDQ5&CQY6M_Y^fQr!)_@~VZa@3+qfyA?8q@~VTdCBIMLof+_AtrOH( zT-*4~uZku7c+BzCoI~jup_Th97n9Svm!}6g+APYL4V#*jxR8H77X*SZBqgoiO{4j z`}QmcI1>4O$T;cjRD|G^$Zz_6vpdvanp|7wo z2L-I$2|g7@w*91$V+2p6*vPrxY=-{OiaJZ_pi}!99NvC7l%-_^9D{R6_pfv4UuFiK zwi&*v`vJmJBSTuF!oc)1(;yMruFo+uOE6h_g)xvI;!CPpiLGITJ&Y+p7dz*mJ@B65 znXR3j$s1=a=1(brQY*51)V9QwO{Mc{Lt7m{yEV=oXeap4c?a5E@W(yDD*p{TPQsybrgRo3aqgvM_Pv!cwZtN2Lx;^7i;8YL ze>o91xwSX@9^V4i0CHKL!k~!9XH_Ef8wGbHKM!p=cTP`jey7 z?&TzqNhAU2zw4qZ=PN66$>HiQaH&uSM8%kUGCFfHI`g=T;yF&4bF1%8r>D&mJBp9U zr*V`gf@u+W6p4ST1`nwAeyZy2+iGxH0ZTGoj}S8V&upCv%c-_H9`CGpk*!kqru+43 z;0hrmJUw41@YcLT^`~DQGNIz?RC>KLet!{T?mf3PH@*c@j=Ahb?g3FgKGk}{W+oqj zjjq(fPf1Mm)U*(8md6?hM;*)VQCH6@|FzpwJoMGtLFCG$@H}QggW!rAzj4J7A)V$s zO7Tw{`;gvV-~so-J@od!nV4Eud88Zk>&KrJJ_;fFeZ~Czj}N(f86VletN4CZ;5m+B z-R?IHPqd#;^&$7>%+}`2jYoMiAi}mbcvo$_GRHmF{^mpJhpZ}Ag|^GY0QhMVgXQtZf8f>%_d=)tJDiJC-K7OB<915e%s7tk z?xVSyhSCTvXlcA}`wtQz>O-#*(N9{!uNfx#jjw@S|9obt44+Uhi3F?%>v|g7dfIqL zNd1~zE!gLk%*Phxs^+?D@LhSVTeEf8zJb@*+UjcKa3YoO#Z7k8d51C>oPnT@d!nM? zK(CzR@(aYd4+l)3dra8goNLcZ5QXp?g^TWTe%lL4esV^``J|ru1%5xR#$3XR^Xkqw z#g$D(pQn)?4V3zWs3gl|vg>ZlqMEmMnEt^#npEzzsA>IehM&w_!r;46a+2K2eCce- z=kg_7(Y&Z`q4Gk}iQqtwIAfpe}=-(y<>Cv0m@cmnMcCFU{wQ2ND2yefu(;iDG zp2I9q9oJlIA7X6W`&@wu&a4zW@56?7R#tGRk0G5hqGC-pHaheE6~oOKkv;Yro>Cd9IVCwe66yj zWlob`P?8JeylrmGUT0y+EhxZ9Dup(@6hX(2Er9(h7m2@Cy}XJ_iQ(rrkWZ1H&&IzY z5(FZsV_k07<&Mq10dKp!6X$o?LJpH49Z_}lnDp@I=!_{5LU~oy#@%P6!uj=#-=TUP zn6;-QKl)>V;%cU>QG(ZjI+5KXYUC7VA+^;_SmW9HnGJTc&ho8~WshAXu;!DLAr;dN zW$4w2-p&1qOO|6Bv{2sD@$#ZyzrKBJHXI@lF2rFQU{qj!#p$(Va(1>_ZO*Wn45q9Z~jlV z^GaPHxCM2k^v_^y*?DN_kAyGS(AM|A^W}_ z9FE_0jutgDpP6}o{`fwAkH^O!^BCg3U+2DGuj_hV&+BgwjQZocp1X*MU#VVR0tg75W#7-DX-ms+z8<3~z+@OB zlJ9=OXes?yTl!8bUlbBDMa}_Rwb`DEB-q`<=2`QTmKhhwGJ>1gIpY`gmw6rBE|045 zSEim@_I&8}+?yN-&4!VTZr`3{^XtN#FRmXSwcAj1?MFfZ(neZ2@B>U(2LVUrQTHnk z?!Wl5nJY86&uun)=xW|eD39$|REfLUM`M(hPf2sz#QI=lk`wLF;-_q`8qx#$CW4dg zF}h@3m6l4CmnA-#un;}IQYrnsA={HdCsoq5pr^MNux5(ul?JKzc!JLG(2l_gKVPzO zq5(LU6Cz9ewO<&yi{@UOlo!W_^y_08X~jc6h(#T!Vc6gD_{_0H2HAn!OLIP1>7CiZ zBdA|bUvvAK_u_OfsOdBI6D-V_w>RBp2;TCktkZSQ7u@vh#|6Dd+0TFEKFO-l80Mz> z8Z-k@yq4kzfZJ&?z0Gt=F681(Y3Za8px+4mo5&~rt?f!_xhula5ew4QpcFRwBP(Uc zTjFU|wq|E%p&9Wt%lwR!tle|aH5bNkKw6-U`e&>(;r8Ekor&leN0c?gM=1+m{X3oywrQ z*s=EdSereo85|G%{f~sKy?FUDsXTHDgM}sv0hDha-862857zcW*G^i|-`^huE2W4z z{}q@hnoGyw%W3)@_z2HTegST*gZ|cB{{=U=tNsu6s8HOVD9`Uu;A95e; z*lkag2Q%Sq#y@|4*UE~M`~Vawh=me`ST3|lSau97im0_@(+vAR)VmhY)~O*quMbkv zNxuMi5s8wDW^DEAhslqeKDCQc6j7__Y!$G`J7YIOd1H)sGU;m z!7pIsAoY+OMXZ`ge6n*qvkRS~YtwC4L9?Hy&N zJ25a7Cb=B_sNL_&Lz7gl=XH}*QMm2_a-UKx`W%|*8%KA_wcu&BQ3W_VpaP~8;ud8N zEVe!7`!z*q#}W{*l}Z!4+Ixd*!hJ>7hTurr4Q3#}yXAQD>czURChO^8%J~_j(H$B3>EMo`JtD03 z%4Vn{-BSWEo&WG)MzWso{s&}xLrW4iKs*$OQ z)jZCdXxvhGbMgF#CD(=C?0nqv{Cl>!aDg*y3oV*7r*_a8KAl>7cy~J0$fLfq)p^@5 zO?mtN`1KFn`}^NxO8*6@&A7O@g{WB* zJHf#~78t*~*qLiXisvn^RIzU-H@~i6WrW)_eRW1kS)5+JsQk_)W7Zpl$W`p-f`H(0i1^%m*1zV1)X z=JQ|{IVvOHwN4b)Zih#0EOxuJxR#T+mIy?@PL7UL9BEyuo0G;42%T<^F@?O@Llz+5 zZkl=2@1#uc7h%z*Z&uY=DN%O~$#>s4<1~WTO>>OV!8~fUr0t+ol)l}ESHHuk`&$~v2h$K1Q}rMDrPCg7##fR|PQSgQT*J`|k06 zJ>SM>0c$xkPMBh}0{wj=TFCqbK~6v6cP2~eA&cX&&74mmuxBvf-H{vC$k=n`_VrPP zxB}Y0ag{bM^fEQ)Tz?787UtC(??+z~`4^l#rX=SoTpYJL(|YQVDK~u&A^YyL;htRF zoVe?p+rO&QbvXFoXnVmE0LPvu67l^2yQLs0@1XQcVhZsJD2|?vxE36gL>Po^6rVAu zC*C`wzmxJzS1XJum8uNVe!IC@QkaW}OOWdSWF&#XGH+n#y#G_EX}YfT5HMC{wnJ6> zjk7hg_ww!Htd=rIIK^QzL)Z;Y)Y#!AOQ)&LN={$jGsuBjr-H8s0#CH3*q+Z#d7u3g z_OHQ`TdC|IN}QjFOk|5@Chz?@?V;JbmZhcR&n=R*=5LQI9S)8cu-h~iIWh>I5Q?>c z;nvAc9JV*E&Invdzj4)vIwXHnu9wrvo7w29W6!`q8FaX0>Cqg>LheTosnG3MTK}qt z1xEt3s8$a-j-D3WI>>k<@9t9?mU#A!n<9xX2LwbsPYkIDK%X3)owTOhV~G{4X8qef zPE+@iH&ovs3UDd0GOXvF26|C%wNb+vi^}L!64br?^0;LUTK;t@kNO?K8|+8jrtl}i zppjgrK3eC&Np%q3a#8@kp=o^USu!t>o&k%mkYiNqv%Kg0DV)XwrJqNr+=$7=WQM%`S(sw) zZ>jgMq_Yw}-jjfU;T4B=VVK3SAdiq@A?bYv>XW(@8QS1J#}_k?DUbDjXM(qVm)7K) z=K^2LJe{Q&A<+%&h&orErYNYg*(tZRW;F87(LF8{$r9BJ?aU1`?|hUkcDL^ubOwa{ zXd5pgLwBrTw*VKu^$wSgCoootmXE>c%sJ0FhEZO>S~^&0Uk{(Mrbc|m);HF@z{}ga z{lwM$UgPZU3KQi~ZN+4TfiA^OQ1pivlldTlrf?m!BKWHiB@*8FhLLG1{7Z4Ezj;XWVqpHrw9 zS$^vUbq{iyu>8fO#e2BtMcssB!vtOvK7fh|NNJDVmXq^uXoXCwl4^aUk1Rz1d)#Xc z=as+>&gnl{awWiNZT32@zmTq_POJqb?bO3vrDkJeQ$p!eefCK3K!dZ2EDu&9-Ov8w zcC+#mQRNxL_!FGauYgh<*rY#KtbICL4<8mhRS4HgzzHj;jdKud(}%*qU_}*L+mB*3 zPG3kSDu$}PPJEv%CZ3`CNcGb$hU#?HUMd1yxpI2{LGvGX!H>TJMn5#{-2XhFs2hk1 z!eRKKbQ@c~&3EY4MXT2wW{TXCT z4_-fiN^*kdU3+_6XM-JcxF8E9t<=hHJY6`T0 zCL9TD?){;wlX#jZ+h836Pna}o(0wmcuYMczW3@gk__m^A595 z(0>~WGESxyOt+Op3=R&0JVNp+ZF}*lIu#YF^JmZMhtLV%xN!hg`^nxAZhJ2P3JG!l zgOZ8F+`h-97JaENVcy!UU>QAh1SEfmbR`xmTU&|l>f~w8X;br=zQWewg*UO9M6#>Z z`2`(hyN>r6sKfY29vT?Mz^EwHXv49yZJnKlw(D38W{rzgN1Y9!gk+LU9j~rwiz?jl zAVdmy4>Elc!ADFzpH0IDJzz)s&@O$`p#eV9ci<>bD zFR0{}Q$e%?VoW`murHn>QziBK_3JQa1o#5;zC?SzdypMlU1?X+NQ9gfvFkv_il zn2pBb?{hu5lEsBFC8YQi`@(TCLb!^5AA~c_0A}ozuHgeEWVBVJJZr~Uy3%A$C1$DS zZ2=QI>}=+N#Eb`Xyaa^}jLb-oDK08Hbb=>?VtWbf2yhw_INwgD-ERP|I{6EilnNg+ z-N;E&0mQb7iZ6r8aT`;RYR_GZQbHhRZHm`VA&Pg~05GyotNAFt%aUNpenRUI+$~{K zc}V>vqV_`&|4!sU_|aj=NPG^h@jtj?1XpF=$vF^yArr5D{{N3#Xo*#$zEmQbj7YL5 z0bL}B>J~c#ep~E@DKSXufwKgJMEG3z3{>ZGmGhjdTX&+nvnHwPE@ir>1s){;ga zv2zD?k+Z_W=ITOq6iI}YXrWg0@mv+1X@hkFUBAu(^UDd*GnMunTk)2iv99y9m|!mi zrpZzBpE5G|`}|91K#?isxlW-}@)NIgd^RwBWx%1L#Z*NcpU8BLSfpYrcX*whHv{M8 zUnT}HSg7JUPaz44HJqv%K^4rx%i9fc0ukOM%5V^ryGPZpXhUqBjb*dXRM5=Vkduo+ssZd&c6tXcYs;b20&PNZp-al&WiJlX(oBsg%KshURz6b93jvjEw&>k;X z80JD0-4~b<8_dTSY!be%+}=Ty|4@rMwW5-$=v{4r9~yrC#fulG;ni_*wU1aN5`io- zt_LEtO=P*EF|^o)MkfY%=cVa?<+luILa<%hPI>l;h#H(QZf>PjdOw34@ogv@jGY!M z53=W5%$x>)MZ6A}H#C;Z4HE~fa>ipPHOP0$+_>RClfW&lCgVuDxIbA{75&}$dYB&X z_|ZsezK-o_lVx4?<&jw1z)^#^iS_bYVpFt$;>n};jAq?}_2%&^cvH7(rQZ%;laYxE z2~pPBg8o=}IXS1tx0{C-61bODWgPea;sTz2OFMmKw?Dsr4Fs8wEQtS1Ctx^UTFG9U z|Fk;n155cLU^Tph6Lm7!5qj3A^u2#SwBj&4;D(pM$7|-QK;pNf8&OwZ-*1A+ADkyp zusg5A=*Ur^$>;9yd?R^0J}dL9Qy3wNBK+EKm%fRX@ISkPXP5tXzV7=AfHD#fVPH!` z#&#u1Zk=(j{W+rbcRzk+sCMoPD~1TiQT6ys({1~vF?GB7^@_EbZdH_n-9)f$6Kz27 zTHlYZyP(mHYyW^*7WK0*?wUW6>X2BMe1jmaOe6lQW*YFTwjkA5r@6 z?&VY1tK%jR9p7r{x6LcGGLpS7mIy_6z}`{VMM*>;wJv@v5FN2vin^90W+Z>FtUB=d zoMUyKa(t_@ynJjB%^?X(%cXNhFCL2*sj>P7{Jbk>#B1b=x^MAqe=u!+Nwn^y4dgM*r4lqKCTqbembG}Hl9cR*!GgYJGcL}Up&SBmwxftJD_t@=v@~VEmo3c z!R+BR?u$bLKeX4u%Z0LanEnPMgMhKh`sfH^JWn#l7nsvFQb*C~g9wq<2v(la(zK#|@cYJVtvQdvxO2!kboHvFKSefpH#`Z@}Qg0|!2 zFr^3v{UnJNC~JJ>ak)gi`4eSw{{tmga!-F5S4}fMeS^lkI)>$mfx#VJ9Ml)CwJv0=Z!tf>8y+nFJN1~`68G@58b%1`+@z7=q>ki)|gsy<8bG~aIMSh z4VQS7m^#>IGkBfrSLA8D7Jbs>-djW)<{!lb0FR#uTTyz0C7AAq! zf#Ms(b`XF7e-Q`6Bz$m6DNx_CP@ca*uPmpgmNKx_JY&;qbV++5z61)18G2zPMtYXr zc4b9H#YZvecn+gIAITmoENcWo5gqUcauT%dul#u(+Pb=!q}nN9A~){|-}a7cCw*=U z*`MA~HxY~}LDBa21-y?M`{$1k3xXhcqcOt{i6DsI2!(RaIy&OcJ?-t*kmqXecysrH zUam^J##NDTjOhOTKrzau{b{KLaj7lO=vq*gU1jNr(f}xht zU+cMYM$7BMu?~kopQ;R#&+0X~pjy2D)J|dC9lAq?>7O?>VQaae?(v3DJvlNm@bUOT zbLfNsGZkhMK@CQUN}t*h3VzwSo&TZ?U{#n8M|~hEHMKuSU*5pL04xTw@CLM(di(p8 zE2`$OjVFb`LDm7fMXxv6-uEQ<)_~AUVT~C@=s92s%S)hS2s~nPnF`bdNZw~Td*^)6 z=%@~Zg@+*oo^lc@c6f!magaLnxVjzzqXxj9PN$8OLhX`d_q$QZ`uNqx&ZyVHz@E+0 zG>-2c6BLV6(mIna$}FP`G_g27%r6@PspXl$w+|JwVQ`bWHdvCU1NG6E5952{`Jp9?gvc2tHT|0G#&3yTggci#!QpV>;U~@lqpxda zb^8FMP#_!vMf>EO(u5K;N9+(*1SV`liP$v*J+%0ZKK)WXD_*7_=9Oh=8OB24J?IiB zu!t#Y`u84i<{J08gT<$(O_g1RPqIt~1O{5epeDdtEBasRExDs$|83>RJ`Fx-{-sMt z_chRt=Mjl`FuzZ0G#*t|L;v#Ki-rCFKPuu62SW{HBJ9r?w*qht&Vo^z`1z$!^fQc^;~EyTqtg`KV%$@!ozGFaM?z!SP-q?2iP zj$W$wZ9`AVEkG&gk^jz(vE`gH@Q_^1GVEmKTFr)us!*2!VIget^v-0(LqGon-u7Jw z%h|KnAqgraWnHM|*8Y{IOf0$YVYm}2cBCdIdba842Y&I1ou2r&2mH1+Wlyr4Vz8FL zZu<@p`N-ea2zt!*^5C6Qb)xmD&rBwhH|9bPG?_59(){Lvkf-@edD3x*IkUcDRj7I` zE`#{PdKD#4U^36(QD%N`Bi+=-PG1nb3HnbLUM_TgAk(a&-zGbH)j=U~GvEO~S6|fO zwM*YbdQ?L{WqkaUg(dtYJ{9&!^}J)u;HsY#M1=1>h`-2t2;cv92}IjRci;N^3k1Bv z^MYI7=%H>?EJp9?`R2Z`;=fZ07489LH{q`EpL>DOt0Fxfi=$J|#=U(P@G2W6CFsaQ zyXf;!97{QCB{WfVL2mB1y4(klTY^|5?l(Uuh;#al*<)82_^+G}V!_uH{Men*ifh0n zL4yP^Oi)0iqOMM($}yDnWNkZM9CreW3ZMfMG>6aPzcJi?QSs+pOU6^A&x(z8y2u^mM20 z3<@1MI5->~9ordzUZn75fGQ+7L+hsGwIdc_*sn_gYsGrowci6t3AhgW-g4yh?exP! ze3Y$|%rjW0PE1I6d^9ds?r|_Eo!dw7l~f~8No5dze!2+wOc zl*{0$bA;@eiea1@NS`$`sR_wWDt7`TYHr^seN&(qc?h4YzM76CV(Jb?q$QuQz>yVy>P2g*+kd zqz`FD`aRK~e~$gxll}0a!#8=>2mDTidG6ouG4_U+w|Q)MiN_CvaTFYLEE~hx2ME~- zG+|0MEpT0Di@BAQ&O0XEZF@K~(yWC0*z(}PoM>QkEnOt}OXnf}jTO~m8{-&S_Isi* zbDaqq#D@n5Q+mn9ibLIa_3#2_z>)1q-)UO5^36pM0cW5FO6*%0%S+p~ev3fU!|aWc zr1}ul)|5sRhpseJ#c*o=fWts`(Y#B^=3+gxD>hjCVa$!-!gcD7XT1;|UQPL7G*ka;i7psI^B*h55Pk#7;l97wFlP`D1 z-A1WA=M1W0OLi+VG>Vmp%HA}`S-mkO=D}vMgIB@j$-cfm-?@_vW!Xnpoz@23BXrNu znidSY!K^kG)x7fyE^?)GFP5^$ZpK$g9^tFhN@#X`CFA&*RjgL0wqZc#H(3m>u;@e~ zmf^ry-^fF@;^~Xq2U(Aj;`_w>@Pk_TQ%Jupr+KZq^d>2#zM&I1d$}N6XE5afDMnOP zv7P_oV5~>-Ju#ym=YG@MU9z^P1!yk2UyqOvzjGenff>T)Dy1h5Lf6K0A*3mSA%?8F z$N`V!`X7a6uEOe}+2axPRM9Js_I1_ zAk$Jl_FNV5NI-P*BbA0?3R&#N{dg zkA65t%v)F%rp@zKhg>p-n)&4P^f?cYBSM+ z{Ec~4wcwynN1j!B@l=}DKFkbLgiY2^cfzQv-q!nZO3PSnp+^*yVlt65lHR_}5g9Yl zo|-+3O|kOQt?%Ala9W%1%Ldz683|9uFugPn^3Ad0J~#Rd*w}@P+<2jU=sZ*%g*U8p zYDB%UD1tYu`mAR_sf}4lEPm$;5w<&t$h8hw8HAl zKpD~GF^T4$j*29*W{S=fUtIbGf7DV!Cs83i6%S zlPzDGJmb9l=+?tVRt)1qqceDwDfnOSfC>*Ho;e}9&l~$^QcAUp^gGV0fQO#7;Qp7( z`5!j_ky7gOQ;0_E3RQ7_;F#C_6@R541XGc$&?^f674rG4UU4JES7XB0W6e*O`Qbj* zr(6ASO#IWe{_oca&*=a816fZY?G(Q#uVNiN@=mNMVJ?;MX0O~}I634M6CkeRlVJcY z9wnn#+r)~lQLdC8CA`~fiKj)?7S_y9Ws4toBcl+boQM(3qbGc#vw;sil2d0Ao+zIp zuH5e(<838Nr%H?AzanIp&9!Z|75+fO!^lWoN7xHQLjlWHQlsoH1&?W!*OqN z@q#evrOKvR4j;*j?=OVI;$eB2yOzf&Q_^a`xyT)+8>FzUqdPontAZ)(EL)mcB`%i6 zz1W;NE7VODIeD?W;52@v>My=q=}|u9bFGw(6=uoKwvJNt#_NUgcl%n(FfpBzF^$-m zU=6=Iq~)|DQBuXjrlr4HL}MfNh!%D>f+H%;VQi}-mN5q7E5469Mqs67bx(^aMtln4 zt=J*D)Jao1=eeS(QY%?8ZmS42Tjk4f^(JNO=+fo)&3ton{Mq%A4m~#~#yhVcL2oQ= z{;K&xlV?G>Xk4jWMfR2%Hbq(JnvvkfxQnLc)`qUU?E+SudOXGw$1+)xb#`Nqf6P(J zol~jIu%EOuGuH8l)pmQynaOp=aOYEu8=?Z!8jo#ODg>q#%AKEcZwTvnKjNcZ&#etFGa-Mg% zcQX{@?d-0K=A$U>KkO62nV-hqlf&q+VB3N!)9M*@X{qpOAq$I{V51@0S<$u=*~*4> zy|8m@`L@vIIg}x4g~oOhrLpl|yRcAWeDikgL*IMofu!<{WYx+X*f5B$1tZxO0sMk+ zYku3~bGWgTD#xu@l$G|gpn#=)s8wCmVzw@_k3BnQc`yUj+Yv0hRn3N6D6Qn9Mx$76 z9tzXc4JMnlIO9Ihe`&TCwdIbov*(#EKW5RNe(C(2Mb`j&Ls;R5^7ric18Z2M0wpbI>U2H!Mgzm_u@+d>Hpt={6l7+ZjQ zmzSwUR8;a}X*5G~+m;ELx%f6}+vzG(g-&fYpKS_SCHoTjo_)CCrnv!o-LY-wLt*)w z?kGyHTDomI=16SuEvMn~&G=cyzLA^aJ{zW*OZkE2*G{a;Y%hn-y3dTH@Oj z9<&|zMt^S$cg;=5C9`ECshp4bEt_ajzf!8L@i@JX^cZ)PbMm3Wpvs>m9KWx z`eSLTC%bu*WqH5lq^+q$`mCP`wj-zWkQhqiXk)qua`_&oqxy2}N=pM;6usTvav^n? zLGv_j>XK}_`cjMfD6SP%lDUMdLg8rB&=yU>R#DxVOWqm{GZzbEX=Ue?qSuPc9hduU zllW|Gc#Wt~i)`EZ8Dk{d^O7jsm`Kz5tUAJ<=Xa4gli7(qh!3h!7#F9SCvJmPy3;Wi zDvmR-wh(hWgNrln`GoMx)hNKqi$v>l-H>d&1L$E~1JhvDzy=gRdxujkQtGmtFHb?`zAToq1ELq7-V0%bobBR^Amp!{azZ&R-{ zrNvh#l`%f2^RU)zgsj<(-U#=xc01OQXeDcCds4%hVT=U#0lkeCLg!i<%i(Oa-|af` zw&=uZDj{aJs6`=WN>d|;jds13ZjG{#eD=_#Fum29@z;S;T6DcsXRIQlMu&URIGu9j z7ApB7FuLVP`53Yb;X%YN}1cLb0FL@3of$5C&IF~vxG zqt+0*GApZ1`Qo|AC1Jay`j1w$?dkm3I5y{nsLg+Le)V^%#o*G^V%b0=K8%s;S^ znE<{Y%U$;<7!aJd2$q%I8rsxu_KR9+K!s&-;!%1=HumPQKi)=KJz;$>=4>QJFm7_^ zz-9lwHRaQA9IVJ4azH8@UArE_78b#I(Gc)}rY!v3q}HxGt(GdA-0y&QP!TguuijR> zYhRVF*RcK76}b@_`hW3fg&xNkcW|8`cKx@Vh$73~gPGd1VBAri{HDkbzQ0dQs&wgy zc?+h9fG7Wx;$Pu0rMDGU%q^ga5FiE;0X{0V=aw9c=s8xM6cfuAOEu#z?JZB@Lv5Q9Sh2J5MV;Yh^o8$*zTV#9$)<-P z=4E`?z(5!BW#UX_&Duo17Tt6K=65=ZzV%m`r&MEVqQu-IJyvB-ZqyIS-Ui}GgJI7QHq1Nkj}mHzqZB#$CGAKD)Mi_ltkKj{y3LFaO)(RG zO-4dRtjG`8g;KI1zFC7USW+tHlphs)OhN(^tsl&-{gu(5dFvD_>@&jUo>L}z7D$&- ztt)(#Vzt~gLUz%mxH&Y^S{#%(y}ta; z1`>mwGg^M8G`%JI4YN_B?(n?UgH%nFGwxnxQcOp%gkMWr_A7w1vJ(#@fLgh%vOOAf zw3_h{KaKOe5w=+7L<({~Ol#Iipk#TiBeT|^=0yKSs%_^sCW*n;4gUh5VFFZ+Ff!_H zt}Rq}e!Q{0YZr?F-|^wqYA&B5a?WlhWlfbKH64v|sEuIEdnB;h%PzrOzS87u|f^~x-`0B1bst(KD)7v?#hSh8BEjKVmrf#*(uHKQ7 z-0BhGk}*QA;^v=DA@#LyRKKjEY=zPJV_{rUubR{ZCszV$2c?I5(o+cgRti+Q)8v z%uBG>T7Eq_{zkr$Pxp4PGo}O+N@gA>y&>_{TCjn&m>F2MYVD=&ZRX5%?xGUfvYB?F z!3fS*KH#I{vAHkVww68qj;sYYVVZiJ@@2$W!$Y#O0XR~ntRj!;PgXAL2JDYID5uQi z?o~w_8rsLhN6%kPVWZ++f5X=nFA^UMyY4*O-cM5hR^?zh$!kXh31GT)|9U zuZ5*$df{j&NKByT?2mhX0XFX9O+9`6!XYo_ib!Z70L-c@FOcIwc^C!BB@gBab2|LrMhwzzB-JCq8VDsNO21mjFzV_%YUFbI75Mjx z-52@1zvRfwBb-Ec4talExftjOQy{)7KxmiwfJfRsW;3#ZfNz8o^k{=*H%DmI?eznZt*kN;q zMDO#qL7#SxG6MtmIsX0(_RGp-TuP!o$pc&c6`PqTGSoI@yy2Y5dVjvm3={uKT^B~l zx9G8IgMxwraL_NH1o<+xIndy&mKE8|N@*o49jatunRoEFf@iB)9yjCM{(f8B_3b2m zeFVdffx4pjuMN}~Z+7;esI&9dOg?&`qvH~ApHS{%$3M{3#beS}P!$g8Xbt<3K$@`& z#yvUEk`H0AJ`$|jGoZxMcq;9q4Qcng9n*h55KO= zJ&Ho|t-khkq+8Q`9YJz}FN?uoQ`u_VQ*V1YU(tuCwaHS4KJDrCZ4YK;b}*Ao@-r}n;e z3wPOq&qQmy>VnaiZEHZE7LYE+lDk0G8>>}rzn!YR?uyCdHs;A5`@N$dZR~8^=gu{( z(s2}Vt%}Ry6ng$*i;+52)6#ZfWRHU6HjK88^9I<^y2}dTp?+xwDYl|&-?vat8=zv$048 zXxDC+l$bbCG?kW(H!u3M<_yW+t8-`+lvj*>{McvJD%DYHYWo@Uz@9a`i|wIdF1IbQ zQQS=R@93NpDjjYjQ>~%J&OD0^L_3UCKS}53rN4Qtt{X(RsMinnDlX6yc0zNu_c-+z z-|-LOpW2&ZG%SJ+xfZQZ;sNi#W6E-henb3=lO-gAxbgug2WrcAG_d@7id=K4#5QQy z^6}ZPILu7??4yEfs>jI9KRv_eeBVWG^xvUFU~8pLwZ71-(w6;nWYu`k@H=JI{~R*H zJ$-5V^1n?|WweTwf4s=_HMA;{RH#zO*LR+I&s&>^E6PpF+j>VmgU!z~vR|0D6dGX1 zy#8~HSR^%t*~&_Zixx!2OfyF3cQp>bd`r3=RZn;LcvIH2Vku z@3w~f_fd?4byHiOh-oCR%?k^yYFox;rqGQ$$ib&9)QC{rtK|oLXUZ1UlyhP@FKOL- zwyKX#PD?Wxe-{lkD%@x%%dy6Js>yMqhZn3T+W=7>Ji%kOzUWusw*w}j$2w@n7|)zO zJqbOXU%Tv+h-#ocVjYZ`+^m57I`ka1YS>;3EPZm>Y)26O-iJRMf2@=Kk zyU_37TT~xadMFTmNCFyw+)q*x_$J1LlY8mr@qXmTDSL)fu{35a(Ev>7w;tz;R zbs=z0BQmrsOl#7&*PS*8uWRYtTzFlZMon!ac*rZU}G* zso%P=IG~3u3RM={yKbv)#q$J!EAP?qu7iHjzDu%5LzW!O+)%t-+g0_AQZ?uUr0eYgh-ARF%+qA3hdkAwbe zgud@A=+!6GkIwANS4dZRBIF=vWv1I$p}hChtY=kx${C8>1P zy-e-;pb>bXWo`aX&0>}?rZta_$*ZXJbahd!F4Ia|MIC#XtpejXhU!K3msNj_!IU6& zC>^QyE6I4VOlw&uOMc(tn^S&P zMo7FyM8MI8NvAQi-Q2qnwRTr`3VIJ#-^&E6U!~p;5e->+ew+5<^Q$h9>mFKI8>;pu z!xon)jZq$=zLWynFcyZf$=D4pj?_DnFcvi*9Bqz5jRY#$l(`|jzi>!A=>UOi56$)h zxp}`jTegv?Wh1VIZB)D-NyWnI70J&RL_rh9r`an)R+%G%MSw^JUo>NBVF7URWrnhn z5{44BK&i>Wf5zWs_Vp|9>aesO@EgR_YxdALif{5+34{O_bYK1FM-qYMTlY|eV*tDB zC?zE$q5%E}4v2gHuZ|9cb%Ne7t$4PmDT^%KD|`!|Hbb>S*5pPK=0q`_h>I{?CL5VSAU>lA;@GKf_L`iQD6 zmwrZUFD?vSwlPD0ySBee z!^Q=E>mfZ0CNHI;G4)wNZX0W!FF7QpnokVSrS~31%lDUQZF%NO+BAl4+K-ZLZ8&cp zle}?5+>e&VYreo?Wzq<-^2p;W*A$88nrzH8X8d_seB4!KBJ_++jEtS~XXXd;_V5j- zE7jzud7eYsY=+Pohq3dX#e`1%O54N-&3Pwa27_<)z34-Jod`$cm?r6ONh6@d@}2CL zR=w8VYNgf+=@~kWHAD-{m>PR`ns{}lc$%tEq^go{IqCFQ4p-(hPmo0tGlIAVM*8sC zxD}r~YM55|5^6@LkMWq@RaNbM8^BoEG;hRDOLyV!m1XRQ zll=JLetLgv5SvCMPX(yQLIVOUA)Y@y3K4w@j4>ac5f%WPo1(t*y|?SRpl^kJvS-1~ zrJb!jT*#(wwz2(aedIYRE=|Yvr9dJ3oaZ0-WJi`3+!y~TODcYVF?nCsALfc zS}z#=GBz^eI<(qrnx3An0S2F|Q;h?3PcRMo5YP*gvKS6E=fLgOhi!2^uv6sunLcWzNw(pa0An2gQU z{miVGV)yZh1gH!~Z=o&fSzs~^%lY$<9Ece&`+mT3kE!J@!;F|Oq;de5Rmb{ZI(yCQ zSn9y`=D?$U?CcJu2KB>2OiTrQNysvtn%Y58^#%1E5!Wlcf@?R6k)aC_3dCiNVAn-T zttQ?BMa3`~S>dT_Qm74ZIQmYhgLQ2>69e+`+1KezzzlrxI?MDsV9e&JG@tT<(d{MS z9HjoTcVSBwow&Dm3}drzoLKFcbl2*;stYq@V=ZUz>gnlG2|C%$Dg$Wpj8KBe&z%lQ z6|pth0Q6KD1TP6=W-+@tTu%WA*O*yZXP;3_f~)!do#Gh|yb<8a=gC|2)LYpc5Ah#= zyD!F7tmT1U%>plG#>q_W**q_*X$KtvdF_6g!W(Acxz>Jvtrw?|Y$zF5-=&|)B)q%Cm-za+GMpFaAl1YRYj{FE&zBKx1&+wbkFM?uyT5AU+hPl4Uh?}7t- zTBqx^Gwh4<%es?p*JCSx73(_4r@k!73Iy|)3_|g3F*WP3j!xDHOQWfD)hPf?r7dMB z!}=gf5+TemjV0YdnZns%#c$on>7k_F93+~pUgh}YFk@)biBus=J5-5@l9P-z)^~-m zgSDH>&;sKWqfze73%%U@VaA#}J%1Xaw8sLmmK2BDwSoQ}npdCYvLzi91^x>fhd`;I zFb!>b>1vN?qif3l3dfU;ElgB;!l4)%8uD$t6&x6-+(M*=4`;v0M*KS*@42+*%wygx zfN6*nD2BXHUav`gV`J%PsNp$gW*FtR?;w@g;>WiaqT}L5yqME|b#x=9R+N=(F@Jaq z`arz*LVUopaNnLac$2+;Cp@?z4YEG6nJI5$nwEBF_4ez8{k`>3>6>q3_wGe|o{RDm z;y-&6TV)?k%qZ@2JlAev=++}hBtiWLc^J286DTpoG&Mo>OC*x1*AfH8i0(sRI=yO# zY49eE`r>_$qouF#WXu=zHZJz}pn8Q|`VmYIn{4wde;V-Bh<;BCLyNc8kyN~jm)YTW zJPYzQLFV%JQeLx|Pf}iXh$22QALB+mE$hWx`v<$B>-T`eRH~;Gx_te7auAOD9O&bA zM3D~ukPtLoUp&QjULO^kwNaP_ahG_ylCk~fzP)>GCEA-i8`gNj6PU}G36HL{TJ0YY zzjHiBT^Ydk`<_}m8Yh#BCnR>IjF45<(;v!c;##E@`KjAW4yS4y-@0d#Gwt3o6f^!d zTT8!Xp^z+3fRi16$7Sr=O&A{$FBR6YOJ++Z16^#@ZTqch*h08X5$5LRwK@q<1)4Ix zH|-C5elHy6xlq*;s_UXT4#?%3*Ox!;X{m_Odo3}%7LK(M^=|K_EVk2*;UR&&$a#b{ky0#|AXs*vmY`=$A{o{XBIcg2R0^ zr#zPhQG6DWeWpxbImbkHc`3x2#E+y^u$-j(Y&a7O3k$g5u7lRl*)cEG;hT^Ka&M)V zbq#u>xtmY$$rWY}6$j{&OF5q}H>(RGW|KnQb$CrfreU9UUPbnJ7(4sTsm&x{D}fCi z1-{N{sj-D|_@O8b?WfHc_lHDURW=|XnhSSdL2H9xfTLXjhbKQV8z=bNpiMwS-gogo zgu>u?^XYgGoATV*TC88!ik}`(Q_q}Ed>~7f7c8W)9w0$cinK8{y1$(*^(~zx>UMejXgtvY@d0XM4JD4`gx|K)EZxX}5s%<-f&UqE zx>S-eJjDf5L51cFA?Pg%pVtmn4>C@UJoaB1NOco-jBA8z910F42p<}!8h#Rhb$H|Cjd=vxSW)^xmbmHg-S5H<-s z3Jsuj@hn)sm3)^P6l=f^0Ixa!PK0EIR+^KO^B|odKPCnIwq?^n#$$)TCkDGaKqU;& zaWrJY>M%kFp5ONhjH?i67LQWYDhiUOM$!q$9}!r;l9R!tGT*`@I93)x;!8xv+P=?Z zCu9N&KLIF11Oj3Ahi!HP68lDinZ`P@{SEswYZWab*QI*#FJbuilp@o?Kk*Me0GXf=~Ix zcM<31o^U8DIJWeAHBGayk7!3Q_#M~Sx#(YDI%`y9moh zE-28|Y=*KPsDc*BG-yw{#fS*}Y6dUh&0x+efz&@B0^?_#MO$xvx2t?T8LZJn$ES@M zo%GU{Ir=CxqjYnX8OK#D|ttx4Kfc7fsF@WO%8(Dvc+Ns%M@&j9bICMQdl7<@H|rGLyg(C`(QN=)Pg9Bmfo zViTPT7B7EB&4Sg;DPVF~oW7%C$zGYR# zczYSFfwWHmiHDM!TA%r2LM-zk5}(id8un2Fwt&7tQ?`vPzNt?1Dgr_dFHbux{;sDf zoAV*o+^NZ#kM0EJnbTVV&@GY}y15h<^0sB(fq0jVU*!)M`=kuW3lX#CV638zB99U{ zi|hJ$=k3FyeRBg4`rkhNof#}_pV9t5T=?g7rIY!(C@@YAXlQDOPlG}3q&-iOsp4bLgiqIgygmb{Y>6L-x+JE zx>7PJE0;50&laVoQl@!j!?9(kIVeikNCoy>*CsAU+;lFR_Q?zTZ|xw=aZ)J>`q{^JCL`AK@X%yxE;btgeKzMm+SAD6{(^76(FTLNPF6JKZSWbjm zZf|GTa*7lUm=-{C>FMbcahh9y__WtYChCTP2cM@LWi1N*7Pwc~JopiU9%~PVO-F|< zryaRYsOp=XFpZ?0 z3km^8V{?c1+eX9{d+EOAfb0#Of%gSX+&^o+Ua-y`Zc$6}+l|-h!e8z>bezb>#1H?)b|!M8A=Chzh9qe)wNKwqB+C7Cqt(?V%;+ zXKDZJ_{Lz_KB2FX^Z22Od-sq6 zuz8Cm24Lt!R-rj{laih%m>Gk>VV5hxdR^;aYHC{S<>qFw>OEYd zX2#{p5hM~>o@I{NyVpNfv2T}XUyPkKdqWDzz2LSd3Weg^h@P4xw|c0no5T5m4HZn= z#|2oMtn4m>0k_&EAP$Mgt*;_BYqj$UBb3O2sHLsP<1uiPDRSIdSN;?LXzKO&yS+!I_34&a?BifIp=uAxMATcZ zP`8^`p=<>^x1Y`|7;J26!p3@$#p9|Km-qLz*5+vQ*5VcHT9HVUSZBO2+NI7oI)55e zJyXkCG*0Xy6{WFHCQCop?FRv9!E~+0Z9b4*E>^4K=|4#(2Xl-rKiG0-H_F7`nvE@6 z*QQW}(Q!Rce*5~|A+ghf@EkM!MW}!S=SkOughZ=vSuD(guzU#@?Nvl`FJRg>%E38#jh34S=L^qb z=$asyq{>2nXULA)AgO<_A%6g2sT}O$ls=D{g<3W}XPCBIV6}mY&hMOB)`njC$XI6( z$|Jh3y>{SV%3CyLe+sez!&lEPS+P4`hYTQC7K=V@w6($7f%q|e(Zt<-eQ7ZD3hOQR Tg`8DU3s-i?7wg0JKJm@p02Zh3 diff --git a/docs/hierarchy_2011_11_12.png b/docs/hierarchy_2011_11_12.png deleted file mode 100644 index b5f8abcc8f9c44491f54a8b3627e75f68fc7c1fd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16324 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 Uy{n}=z>t;}R}jm7_R{_T0j2oIIsgCw diff --git a/docs/review_2011_11_12_alex.md b/docs/review_2011_11_12_alex.md deleted file mode 100644 index 38ffda2..0000000 --- a/docs/review_2011_11_12_alex.md +++ /dev/null @@ -1,192 +0,0 @@ -Alex's Code Review, 2011.11.12 -============================== - -Overall hierarchy ------------------- - -Generally is OK. Like that `Object` and `Component` are now separated. -I've generated 2 diagrams under `docs/` to see it better as a whole. - -> The purpose of separating `Object` from `Component` is to make `Object` -> a super-light base class that supports properties defined by getter/setters. -> Note that `Component` is a bit of heavy because it uses two extra member -> variables to support events and behaviors. - - -Object ------- - -### property feature - -Is it OK that `canGetProperty` and `canSetProperty` will return `false` for real -class members? - -> Added $checkVar parameter - -### callbacks and expressions - -We're using 5.3. What's the reason to support `eval()` in `evaluateExpression` if -we have anonymous functions? Is that for storing code as string inside of DB (RBAC)? - -If we're going to get rid of `eval()`, cosider remaning method to something about callback. -If not then we definitely need to use anonymous functions in API docs and the guide -where possible. - -> The purpose of evaluateExpression() is to provide a way of evaluating a PHP expression -> in the context of an object. Will remove it before release if we find no use of it. - ->> mdomba: ->> As eval() is controversial, and anonymous functions can replace all Yii 1 usage of eval() ->> how about removing it from the beginning and add it only if we find it necessary. ->> This way we would not be tempted to stick with eval() and will be forced to first try to find alternatives - -### Object::create() - -#### `__construct` issue - -Often a class doesn't have `__construct` implementation and `stdClass` doesn't have -default one either but Object::create() always expects constructor to be -defined. See `ObjectTest`. Either `method_exists` call or `Object::__construct` needed. - -> Added Object::__construct. - -#### How to support object factory like we do with CWidgetFactory? - -~~~ -class ObjectConfig -{ - public function configure($class) - { - $config = $this->load($class); - // apply config to $class - } - - private function load($class) - { - // get class properties from a config file - // in this method we need to walk all the - // inheritance hierarchy down to Object itself - return array( - 'property' => 'value', - // … - ); - } -} -~~~ - -Then we need to add `__construct` to `Object` (or implement `Initalbe`): - -~~~ -class Object -{ - public function __construct() - { - $conf = new ObjectConfig(); - $conf->configure($this); - } -} -~~~ - -This way we'll be able to set defaults for any object. - -> The key issue here is about how to process the config file. Clearly, we cannot -> do this for every type of component because it would mean an extra file access -> for every component type - -#### Do we need to support lazy class injection? - -Currently there's no way to lazy-inject class into another class property via -config. Do we need it? If yes then we can probably extend component config to support -the following: - -~~~ -class Foo extends Object -{ - public $prop; -} - -class Bar extends Object -{ - public $prop; -} - -$config = array( - 'prop' => array( - 'class' => 'Bar', - 'prop' => 'Hello!', - ), -); - -$foo = Foo::create($config); -echo $foo->bar->prop; -// will output Hello! -~~~ - -Should it support infinite nesting level? - -> I don't think we need this. Foo::$prop cannot be an object unless it needs it to be. -> In that case, it can be defined with a setter in which it can handle the object creation -> based on a configuration array. This is a bit inconvenient, but I think such usage is -> not very common. - -### Why `Event` is `Object`? - -There's no need to extend from `Object`. Is there a plan to use `Object` features -later? - -> To use properties defined via getter/setter. - - -Behaviors ---------- - -Overall I wasn't able to use behaviors. See `BehaviorTest`. - -### Should behaviors be able to define events for owner components? - -Why not? Should be a very good feature in order to make behaviors customizable. - -> It's a bit hard to implement it efficiently. I tend not to support it for now -> unless enough people are requesting for it. - -### Multiple behaviors can be attached to the same component - -What if we'll have multiple methods / properties / events with the same name? - -> The first one takes precedence. This is the same as we do in 1.1. - -### How to use Behavior::attach? - -Looks like it is used by `Component::attachBehavior` but can't be used without it. -Why it's public then? Can we move it to `Component?` - -> It's public because it is called by Component. It is in Behavior such that -> it can be overridden by behavior classes to customize the attach process. - -Events ------- - -Class itself looks OK. Component part is OK as well but I've not tested -it carefully. Overall it seems concept is the same as in Yii1. - -### Event declaration: the on-method is mostly repetitive for every event. Should we choose a different way of declaring events? - -Maybe. People complained previously about too many code for event declaration. - -### Should we implement some additional event mechanism, such as global events? - -Why use two different implementations in a single application? - -Exceptions ----------- - -- Should we convert all errors, warnings and notices to exceptions? - -> I think not. We used to do this in early versions of 1.0. We found sometimes -> very mysterious things would happen which makes error fixing harder rather than -> easier. - -Coding style ------------- - -See `docs/code_style.md`. \ No newline at end of file From 3bd186deebfed0a0cb3f286114daab98bc1a7340 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Wed, 3 Apr 2013 23:23:32 -0400 Subject: [PATCH 029/104] refactored validators. --- framework/base/Module.php | 5 +- framework/validators/BooleanValidator.php | 29 ++++----- framework/validators/CaptchaValidator.php | 56 +++++++++--------- framework/validators/CompareValidator.php | 69 ++++++++++++++-------- framework/validators/DefaultValueValidator.php | 15 ++--- framework/validators/EmailValidator.php | 16 +---- framework/validators/ExistValidator.php | 45 ++++++++------ framework/validators/FilterValidator.php | 20 ++++++- framework/validators/InlineValidator.php | 21 +++++-- framework/validators/NumberValidator.php | 28 +++++---- framework/validators/RangeValidator.php | 45 +++++++------- .../validators/RegularExpressionValidator.php | 44 ++++++++------ framework/validators/RequiredValidator.php | 21 +++++++ framework/validators/StringValidator.php | 64 ++++++++++++-------- framework/validators/UniqueValidator.php | 9 --- framework/validators/UrlValidator.php | 24 +++----- framework/validators/Validator.php | 19 +++++- 17 files changed, 309 insertions(+), 221 deletions(-) diff --git a/framework/base/Module.php b/framework/base/Module.php index 3e7eb04..296494d 100644 --- a/framework/base/Module.php +++ b/framework/base/Module.php @@ -580,8 +580,9 @@ abstract class Module extends Component * instance of it. * * @param string $route the route consisting of module, controller and action IDs. - * @return array|boolean if the controller is created successfully, it will be returned together - * with the remainder of the route which represents the action ID. Otherwise false will be returned. + * @return array|boolean If the controller is created successfully, it will be returned together + * with the requested action ID. Otherwise false will be returned. + * @throws InvalidConfigException if the controller class and its file do not match. */ public function createController($route) { diff --git a/framework/validators/BooleanValidator.php b/framework/validators/BooleanValidator.php index 7a7b68f..b441108 100644 --- a/framework/validators/BooleanValidator.php +++ b/framework/validators/BooleanValidator.php @@ -7,6 +7,8 @@ namespace yii\validators; +use Yii; + /** * BooleanValidator checks if the attribute value is a boolean value. * @@ -32,11 +34,6 @@ class BooleanValidator extends Validator * Defaults to false, meaning only the value needs to be matched. */ public $strict = false; - /** - * @var boolean whether the attribute value can be null or empty. Defaults to true, - * meaning that if the attribute is empty, it is considered valid. - */ - public $allowEmpty = true; /** * Validates the attribute of the object. @@ -47,12 +44,8 @@ class BooleanValidator extends Validator public function validateAttribute($object, $attribute) { $value = $object->$attribute; - if ($this->allowEmpty && $this->isEmpty($value)) { - return; - } - if (!$this->strict && $value != $this->trueValue && $value != $this->falseValue - || $this->strict && $value !== $this->trueValue && $value !== $this->falseValue) { - $message = ($this->message !== null) ? $this->message : \Yii::t('yii|{attribute} must be either {true} or {false}.'); + if (!$this->validateValue($value)) { + $message = $this->message !== null ? $this->message : Yii::t('yii|{attribute} must be either "{true}" or "{false}".'); $this->addError($object, $attribute, $message, array( '{true}' => $this->trueValue, '{false}' => $this->falseValue, @@ -60,13 +53,15 @@ class BooleanValidator extends Validator } } + /** + * Validates the given value. + * @param mixed $value the value to be validated. + * @return boolean whether the value is valid. + */ public function validateValue($value) { - if ($this->allowEmpty && $this->isEmpty($value)) { - return; - } - return ($this->strict || $value == $this->trueValue || $value == $this->falseValue) - && (!$this->strict || $value === $this->trueValue || $value === $this->falseValue); + return $this->strict && ($value == $this->trueValue || $value == $this->falseValue) + || !$this->strict && ($value === $this->trueValue || $value === $this->falseValue); } /** @@ -77,7 +72,7 @@ class BooleanValidator extends Validator */ public function clientValidateAttribute($object, $attribute) { - $message = ($this->message !== null) ? $this->message : \Yii::t('yii|{attribute} must be either {true} or {false}.'); + $message = ($this->message !== null) ? $this->message : Yii::t('yii|{attribute} must be either "{true}" or "{false}".'); $message = strtr($message, array( '{attribute}' => $object->getAttributeLabel($attribute), '{value}' => $object->$attribute, diff --git a/framework/validators/CaptchaValidator.php b/framework/validators/CaptchaValidator.php index 3f31f77..f35b332 100644 --- a/framework/validators/CaptchaValidator.php +++ b/framework/validators/CaptchaValidator.php @@ -7,6 +7,9 @@ namespace yii\validators; +use Yii; +use yii\base\InvalidConfigException; + /** * CaptchaValidator validates that the attribute value is the same as the verification code displayed in the CAPTCHA. * @@ -22,16 +25,10 @@ class CaptchaValidator extends Validator */ public $caseSensitive = false; /** - * @var string the ID of the action that renders the CAPTCHA image. Defaults to 'captcha', - * meaning the `captcha` action declared in the current controller. - * This can also be a route consisting of controller ID and action ID (e.g. 'site/captcha'). - */ - public $captchaAction = 'captcha'; - /** - * @var boolean whether the attribute value can be null or empty. - * Defaults to false, meaning the attribute is invalid if it is empty. + * @var string the route of the controller action that renders the CAPTCHA image. */ - public $allowEmpty = false; + public $captchaAction = 'site/captcha'; + /** * Validates the attribute of the object. @@ -42,36 +39,39 @@ class CaptchaValidator extends Validator public function validateAttribute($object, $attribute) { $value = $object->$attribute; - if ($this->allowEmpty && $this->isEmpty($value)) { - return; - } - $captcha = $this->getCaptchaAction(); - if (!$captcha->validate($value, $this->caseSensitive)) { - $message = $this->message !== null ? $this->message : \Yii::t('yii|The verification code is incorrect.'); + if (!$this->validateValue($value)) { + $message = $this->message !== null ? $this->message : Yii::t('yii|The verification code is incorrect.'); $this->addError($object, $attribute, $message); } } /** + * Validates the given value. + * @param mixed $value the value to be validated. + * @return boolean whether the value is valid. + */ + public function validateValue($value) + { + $captcha = $this->getCaptchaAction(); + return $captcha->validate($value, $this->caseSensitive); + } + + /** * Returns the CAPTCHA action object. - * @return CCaptchaAction the action object + * @return CaptchaAction the action object */ public function getCaptchaAction() { - if (strpos($this->captchaAction, '/') !== false) { // contains controller or module - $ca = \Yii::$app->createController($this->captchaAction); - if ($ca !== null) { - list($controller, $actionID) = $ca; - $action = $controller->createAction($actionID); + $ca = Yii::$app->createController($this->captchaAction); + if ($ca !== false) { + /** @var \yii\base\Controller $controller */ + list($controller, $actionID) = $ca; + $action = $controller->createAction($actionID); + if ($action !== null) { + return $action; } - } else { - $action = \Yii::$app->getController()->createAction($this->captchaAction); - } - - if ($action === null) { - throw new \yii\base\Exception('Invalid captcha action ID: ' . $this->captchaAction); } - return $action; + throw new InvalidConfigException('Invalid CAPTCHA action ID: ' . $this->captchaAction); } /** diff --git a/framework/validators/CompareValidator.php b/framework/validators/CompareValidator.php index 43f2edf..3c85367 100644 --- a/framework/validators/CompareValidator.php +++ b/framework/validators/CompareValidator.php @@ -6,6 +6,7 @@ */ namespace yii\validators; + use Yii; use yii\base\InvalidConfigException; @@ -45,23 +46,12 @@ class CompareValidator extends Validator */ public $compareValue; /** - * @var boolean whether the comparison is strict (both value and type must be the same.) - * Defaults to false. - */ - public $strict = false; - /** - * @var boolean whether the attribute value can be null or empty. Defaults to false. - * If this is true, it means the attribute is considered valid when it is empty. - */ - public $allowEmpty = false; - /** - * @var string the operator for comparison. Defaults to '='. - * The followings are valid operators: - * - * - `=` or `==`: validates to see if the two values are equal. If [[strict]] is true, the comparison - * will be done in strict mode (i.e. checking value type as well). - * - `!=`: validates to see if the two values are NOT equal. If [[strict]] is true, the comparison - * will be done in strict mode (i.e. checking value type as well). + * @var string the operator for comparison. The following operators are supported: + * + * - '==': validates to see if the two values are equal. The comparison is done is non-strict mode. + * - '===': validates to see if the two values are equal. The comparison is done is strict mode. + * - '!=': validates to see if the two values are NOT equal. The comparison is done is non-strict mode. + * - '!==': validates to see if the two values are NOT equal. The comparison is done is strict mode. * - `>`: validates to see if the value being validated is greater than the value being compared with. * - `>=`: validates to see if the value being validated is greater than or equal to the value being compared with. * - `<`: validates to see if the value being validated is less than the value being compared with. @@ -79,9 +69,6 @@ class CompareValidator extends Validator public function validateAttribute($object, $attribute) { $value = $object->$attribute; - if ($this->allowEmpty && $this->isEmpty($value)) { - return; - } if ($this->compareValue !== null) { $compareLabel = $compareValue = $this->compareValue; } else { @@ -91,15 +78,26 @@ class CompareValidator extends Validator } switch ($this->operator) { - case '=': case '==': - if (($this->strict && $value !== $compareValue) || (!$this->strict && $value != $compareValue)) { + if ($value != $compareValue) { + $message = ($this->message !== null) ? $this->message : Yii::t('yii|{attribute} must be repeated exactly.'); + $this->addError($object, $attribute, $message, array('{compareAttribute}' => $compareLabel)); + } + break; + case '===': + if ($value !== $compareValue) { $message = ($this->message !== null) ? $this->message : Yii::t('yii|{attribute} must be repeated exactly.'); $this->addError($object, $attribute, $message, array('{compareAttribute}' => $compareLabel)); } break; case '!=': - if (($this->strict && $value === $compareValue) || (!$this->strict && $value == $compareValue)) { + if ($value == $compareValue) { + $message = ($this->message !== null) ? $this->message : Yii::t('yii|{attribute} must not be equal to "{compareValue}".'); + $this->addError($object, $attribute, $message, array('{compareAttribute}' => $compareLabel, '{compareValue}' => $compareValue)); + } + break; + case '!==': + if ($value === $compareValue) { $message = ($this->message !== null) ? $this->message : Yii::t('yii|{attribute} must not be equal to "{compareValue}".'); $this->addError($object, $attribute, $message, array('{compareAttribute}' => $compareLabel, '{compareValue}' => $compareValue)); } @@ -134,6 +132,31 @@ class CompareValidator extends Validator } /** + * Validates the given value. + * @param mixed $value the value to be validated. + * @return boolean whether the value is valid. + */ + public function validateValue($value) + { + if ($this->compareValue === null) { + throw new InvalidConfigException('CompareValidator::compareValue must be set.'); + } + + switch ($this->operator) { + case '==': return $value == $this->compareValue; + case '===': return $value === $this->compareValue; + case '!=': return $value != $this->compareValue; + case '!==': return $value !== $this->compareValue; + case '>': return $value > $this->compareValue; + case '>=': return $value >= $this->compareValue; + case '<': return $value < $this->compareValue; + case '<=': return $value <= $this->compareValue; + default: + throw new InvalidConfigException("Unknown operator \"{$this->operator}\""); + } + } + + /** * Returns the JavaScript needed for performing client-side validation. * @param \yii\base\Model $object the data object being validated * @param string $attribute the name of the attribute to be validated diff --git a/framework/validators/DefaultValueValidator.php b/framework/validators/DefaultValueValidator.php index be06768..185dbd4 100644 --- a/framework/validators/DefaultValueValidator.php +++ b/framework/validators/DefaultValueValidator.php @@ -10,12 +10,8 @@ namespace yii\validators; /** * DefaultValueValidator sets the attribute to be the specified default value. * - * By default, when the attribute being validated is [[isEmpty|empty]], the validator - * will assign a default [[value]] to it. However, if [[setOnEmpty]] is false, the validator - * will always assign the default [[value]] to the attribute, no matter it is empty or not. - * * DefaultValueValidator is not really a validator. It is provided mainly to allow - * specifying attribute default values in a dynamic way. + * specifying attribute default values when they are empty. * * @author Qiang Xue * @since 2.0 @@ -27,11 +23,10 @@ class DefaultValueValidator extends Validator */ public $value; /** - * @var boolean whether to set the default [[value]] only when the attribute is [[isEmpty|empty]]. - * Defaults to true. If false, the attribute will always be assigned with the default [[value]], - * no matter it is empty or not. + * @var boolean this property is overwritten to be false so that this validator will + * be applied when the value being validated is empty. */ - public $setOnEmpty = true; + public $skipOnEmpty = false; /** * Validates the attribute of the object. @@ -40,7 +35,7 @@ class DefaultValueValidator extends Validator */ public function validateAttribute($object, $attribute) { - if (!$this->setOnEmpty || $this->isEmpty($object->$attribute)) { + if ($this->isEmpty($object->$attribute)) { $object->$attribute = $this->value; } } diff --git a/framework/validators/EmailValidator.php b/framework/validators/EmailValidator.php index d1d2257..396c25f 100644 --- a/framework/validators/EmailValidator.php +++ b/framework/validators/EmailValidator.php @@ -42,11 +42,6 @@ class EmailValidator extends Validator * Defaults to false. */ public $checkPort = false; - /** - * @var boolean whether the attribute value can be null or empty. Defaults to true, - * meaning that if the attribute is empty, it is considered valid. - */ - public $allowEmpty = true; /** * Validates the attribute of the object. @@ -57,9 +52,6 @@ class EmailValidator extends Validator public function validateAttribute($object, $attribute) { $value = $object->$attribute; - if ($this->allowEmpty && $this->isEmpty($value)) { - return; - } if (!$this->validateValue($value)) { $message = ($this->message !== null) ? $this->message : \Yii::t('yii|{attribute} is not a valid email address.'); $this->addError($object, $attribute, $message); @@ -67,11 +59,9 @@ class EmailValidator extends Validator } /** - * Validates a static value to see if it is a valid email. - * Note that this method does not respect [[allowEmpty]] property. - * This method is provided so that you can call it directly without going through the model validation rule mechanism. - * @param mixed $value the value to be validated - * @return boolean whether the value is a valid email + * Validates the given value. + * @param mixed $value the value to be validated. + * @return boolean whether the value is valid. */ public function validateValue($value) { diff --git a/framework/validators/ExistValidator.php b/framework/validators/ExistValidator.php index 8df3e19..b39be56 100644 --- a/framework/validators/ExistValidator.php +++ b/framework/validators/ExistValidator.php @@ -6,6 +6,8 @@ */ namespace yii\validators; + +use Yii; use yii\base\InvalidConfigException; /** @@ -34,11 +36,6 @@ class ExistValidator extends Validator * @see className */ public $attributeName; - /** - * @var boolean whether the attribute value can be null or empty. Defaults to true, - * meaning that if the attribute is empty, it is considered valid. - */ - public $allowEmpty = true; /** * Validates the attribute of the object. @@ -46,29 +43,41 @@ class ExistValidator extends Validator * * @param \yii\db\ActiveRecord $object the object being validated * @param string $attribute the attribute being validated - * @throws InvalidConfigException if table doesn't have column specified */ public function validateAttribute($object, $attribute) { $value = $object->$attribute; - if ($this->allowEmpty && $this->isEmpty($value)) { - return; - } /** @var $className \yii\db\ActiveRecord */ - $className = ($this->className === null) ? get_class($object) : \Yii::import($this->className); - $attributeName = ($this->attributeName === null) ? $attribute : $this->attributeName; - $table = $className::getTableSchema(); - if (($column = $table->getColumn($attributeName)) === null) { - throw new InvalidConfigException('Table "' . $table->name . '" does not have a column named "' . $attributeName . '"'); - } - + $className = $this->className === null ? get_class($object) : Yii::import($this->className); + $attributeName = $this->attributeName === null ? $attribute : $this->attributeName; $query = $className::find(); - $query->where(array($column->name => $value)); + $query->where(array($attributeName => $value)); if (!$query->exists()) { - $message = ($this->message !== null) ? $this->message : \Yii::t('yii|{attribute} "{value}" is invalid.'); + $message = $this->message !== null ? $this->message : Yii::t('yii|{attribute} "{value}" is invalid.'); $this->addError($object, $attribute, $message); } } + + /** + * Validates the given value. + * @param mixed $value the value to be validated. + * @return boolean whether the value is valid. + * @throws InvalidConfigException if either [[className]] or [[attributeName]] is not set. + */ + public function validateValue($value) + { + if ($this->className === null) { + throw new InvalidConfigException('The "className" property must be set.'); + } + if ($this->attributeName === null) { + throw new InvalidConfigException('The "attributeName" property must be set.'); + } + /** @var $className \yii\db\ActiveRecord */ + $className = $this->className; + $query = $className::find(); + $query->where(array($this->attributeName => $value)); + return $query->exists(); + } } diff --git a/framework/validators/FilterValidator.php b/framework/validators/FilterValidator.php index c891979..72a9a9d 100644 --- a/framework/validators/FilterValidator.php +++ b/framework/validators/FilterValidator.php @@ -38,6 +38,23 @@ class FilterValidator extends Validator * ~~~ */ public $filter; + /** + * @var boolean this property is overwritten to be false so that this validator will + * be applied when the value being validated is empty. + */ + public $skipOnEmpty = false; + + /** + * Initializes the validator. + * @throws InvalidConfigException if [[filter]] is not set. + */ + public function init() + { + parent::init(); + if ($this->filter === null) { + throw new InvalidConfigException('The "filter" property must be set.'); + } + } /** * Validates the attribute of the object. @@ -48,9 +65,6 @@ class FilterValidator extends Validator */ public function validateAttribute($object, $attribute) { - if ($this->filter === null) { - throw new InvalidConfigException('The "filter" property must be specified with a valid callback.'); - } $object->$attribute = call_user_func($this->filter, $object->$attribute); } } diff --git a/framework/validators/InlineValidator.php b/framework/validators/InlineValidator.php index 5c12d52..3689a2f 100644 --- a/framework/validators/InlineValidator.php +++ b/framework/validators/InlineValidator.php @@ -25,8 +25,9 @@ namespace yii\validators; class InlineValidator extends Validator { /** - * @var string the name of the validation method defined in the - * \yii\base\Model class + * @var string|\Closure an anonymous function or the name of a model class method that will be + * called to perform the actual validation. Note that if you use anonymous function, you cannot + * use `$this` in it unless you are using PHP 5.4 or above. */ public $method; /** @@ -34,8 +35,8 @@ class InlineValidator extends Validator */ public $params; /** - * @var string the name of the method that returns the client validation code (see [[clientValidateAttribute()]] - * for details on how to return client validation code). The signature of the method should be like the following: + * @var string|\Closure an anonymous function or the name of a model class method that returns the client validation code. + * The signature of the method should be like the following: * * ~~~ * function foo($attribute) @@ -45,6 +46,8 @@ class InlineValidator extends Validator * ~~~ * * where `$attribute` refers to the attribute name to be validated. + * + * Please refer to [[clientValidateAttribute()]] for details on how to return client validation code. */ public $clientValidate; @@ -56,7 +59,10 @@ class InlineValidator extends Validator public function validateAttribute($object, $attribute) { $method = $this->method; - $object->$method($attribute, $this->params); + if (is_string($method)) { + $method = array($object, $method); + } + call_user_func($method, $attribute, $this->params); } /** @@ -82,7 +88,10 @@ class InlineValidator extends Validator { if ($this->clientValidate !== null) { $method = $this->clientValidate; - return $object->$method($attribute); + if (is_string($method)) { + $method = array($object, $method); + } + return call_user_func($method, $attribute); } else { return null; } diff --git a/framework/validators/NumberValidator.php b/framework/validators/NumberValidator.php index 89363fb..6219bdb 100644 --- a/framework/validators/NumberValidator.php +++ b/framework/validators/NumberValidator.php @@ -26,11 +26,6 @@ class NumberValidator extends Validator */ public $integerOnly = false; /** - * @var boolean whether the attribute value can be null or empty. Defaults to true, - * meaning that if the attribute is empty, it is considered valid. - */ - public $allowEmpty = true; - /** * @var integer|float upper limit of the number. Defaults to null, meaning no upper limit. */ public $max; @@ -66,9 +61,6 @@ class NumberValidator extends Validator public function validateAttribute($object, $attribute) { $value = $object->$attribute; - if ($this->allowEmpty && $this->isEmpty($value)) { - return; - } if ($this->integerOnly) { if (!preg_match($this->integerPattern, "$value")) { $message = $this->message !== null ? $this->message : Yii::t('yii|{attribute} must be an integer.'); @@ -81,16 +73,28 @@ class NumberValidator extends Validator } } if ($this->min !== null && $value < $this->min) { - $message = $this->tooSmall !== null ? $this->tooSmall : Yii::t('yii|{attribute} is too small (minimum is {min}).'); + $message = $this->tooSmall !== null ? $this->tooSmall : Yii::t('yii|{attribute} must be no less than {min}.'); $this->addError($object, $attribute, $message, array('{min}' => $this->min)); } if ($this->max !== null && $value > $this->max) { - $message = $this->tooBig !== null ? $this->tooBig : Yii::t('yii|{attribute} is too big (maximum is {max}).'); + $message = $this->tooBig !== null ? $this->tooBig : Yii::t('yii|{attribute} must be no greater than {max}.'); $this->addError($object, $attribute, $message, array('{max}' => $this->max)); } } /** + * Validates the given value. + * @param mixed $value the value to be validated. + * @return boolean whether the value is valid. + */ + public function validateValue($value) + { + return preg_match($this->integerOnly ? $this->integerPattern : $this->numberPattern, "$value") + && ($this->min === null || $value >= $this->min) + && ($this->max === null || $value <= $this->max); + } + + /** * Returns the JavaScript needed for performing client-side validation. * @param \yii\base\Model $object the data object being validated * @param string $attribute the name of the attribute to be validated. @@ -116,7 +120,7 @@ if(!value.match($pattern)) { "; if ($this->min !== null) { if (($tooSmall = $this->tooSmall) === null) { - $tooSmall = Yii::t('yii|{attribute} is too small (minimum is {min}).'); + $tooSmall = Yii::t('yii|{attribute} must be no less than {min}.'); } $tooSmall = strtr($tooSmall, array( '{attribute}' => $label, @@ -131,7 +135,7 @@ if(value<{$this->min}) { } if ($this->max !== null) { if (($tooBig = $this->tooBig) === null) { - $tooBig = Yii::t('yii|{attribute} is too big (maximum is {max}).'); + $tooBig = Yii::t('yii|{attribute} must be no greater than {max}.'); } $tooBig = strtr($tooBig, array( '{attribute}' => $label, diff --git a/framework/validators/RangeValidator.php b/framework/validators/RangeValidator.php index e23567c..0498a55 100644 --- a/framework/validators/RangeValidator.php +++ b/framework/validators/RangeValidator.php @@ -29,56 +29,61 @@ class RangeValidator extends Validator */ public $strict = false; /** - * @var boolean whether the attribute value can be null or empty. Defaults to true, - * meaning that if the attribute is empty, it is considered valid. - */ - public $allowEmpty = true; - /** * @var boolean whether to invert the validation logic. Defaults to false. If set to true, * the attribute value should NOT be among the list of values defined via [[range]]. **/ public $not = false; /** + * Initializes the validator. + * @throws InvalidConfigException if [[range]] is not set. + */ + public function init() + { + parent::init(); + if (!is_array($this->range)) { + throw new InvalidConfigException('The "range" property must be set.'); + } + } + + /** * Validates the attribute of the object. * If there is any error, the error message is added to the object. * @param \yii\base\Model $object the object being validated * @param string $attribute the attribute being validated - * @throws InvalidConfigException if the "range" property is not an array */ public function validateAttribute($object, $attribute) { $value = $object->$attribute; - if ($this->allowEmpty && $this->isEmpty($value)) { - return; - } - if (!is_array($this->range)) { - throw new InvalidConfigException('The "range" property must be specified as an array.'); - } + $message = $this->message !== null ? $this->message : \Yii::t('yii|{attribute} is invalid.'); if (!$this->not && !in_array($value, $this->range, $this->strict)) { - $message = ($this->message !== null) ? $this->message : \Yii::t('yii|{attribute} should be in the list.'); $this->addError($object, $attribute, $message); } elseif ($this->not && in_array($value, $this->range, $this->strict)) { - $message = ($this->message !== null) ? $this->message : \Yii::t('yii|{attribute} should NOT be in the list.'); $this->addError($object, $attribute, $message); } } /** + * Validates the given value. + * @param mixed $value the value to be validated. + * @return boolean whether the value is valid. + */ + public function validateValue($value) + { + return !$this->not && in_array($value, $this->range, $this->strict) + || $this->not && !in_array($value, $this->range, $this->strict); + } + + /** * Returns the JavaScript needed for performing client-side validation. * @param \yii\base\Model $object the data object being validated * @param string $attribute the name of the attribute to be validated. * @return string the client-side validation script. - * @throws InvalidConfigException if the "range" property is not an array */ public function clientValidateAttribute($object, $attribute) { - if (!is_array($this->range)) { - throw new InvalidConfigException('The "range" property must be specified as an array.'); - } - if (($message = $this->message) === null) { - $message = $this->not ? \Yii::t('yii|{attribute} should NOT be in the list.') : \Yii::t('yii|{attribute} should be in the list.'); + $message = \Yii::t('yii|{attribute} is invalid.'); } $message = strtr($message, array( '{attribute}' => $object->getAttributeLabel($attribute), diff --git a/framework/validators/RegularExpressionValidator.php b/framework/validators/RegularExpressionValidator.php index df2b657..b0811e9 100644 --- a/framework/validators/RegularExpressionValidator.php +++ b/framework/validators/RegularExpressionValidator.php @@ -7,6 +7,8 @@ namespace yii\validators; +use yii\base\InvalidConfigException; + /** * RegularExpressionValidator validates that the attribute value matches the specified [[pattern]]. * @@ -22,32 +24,33 @@ class RegularExpressionValidator extends Validator */ public $pattern; /** - * @var boolean whether the attribute value can be null or empty. Defaults to true, - * meaning that if the attribute is empty, it is considered valid. - */ - public $allowEmpty = true; - /** * @var boolean whether to invert the validation logic. Defaults to false. If set to true, * the regular expression defined via [[pattern]] should NOT match the attribute value. + * @throws InvalidConfigException if the "pattern" is not a valid regular expression **/ public $not = false; /** + * Initializes the validator. + * @throws InvalidConfigException if [[pattern]] is not set. + */ + public function init() + { + parent::init(); + if ($this->pattern === null) { + throw new InvalidConfigException('The "pattern" property must be set.'); + } + } + + /** * Validates the attribute of the object. * If there is any error, the error message is added to the object. * @param \yii\base\Model $object the object being validated * @param string $attribute the attribute being validated - * @throws \yii\base\Exception if the "pattern" is not a valid regular expression */ public function validateAttribute($object, $attribute) { $value = $object->$attribute; - if ($this->allowEmpty && $this->isEmpty($value)) { - return; - } - if ($this->pattern === null) { - throw new \yii\base\Exception('The "pattern" property must be specified with a valid regular expression.'); - } if ((!$this->not && !preg_match($this->pattern, $value)) || ($this->not && preg_match($this->pattern, $value))) { $message = ($this->message !== null) ? $this->message : \Yii::t('yii|{attribute} is invalid.'); $this->addError($object, $attribute, $message); @@ -55,18 +58,25 @@ class RegularExpressionValidator extends Validator } /** + * Validates the given value. + * @param mixed $value the value to be validated. + * @return boolean whether the value is valid. + */ + public function validateValue($value) + { + return !$this->not && preg_match($this->pattern, $value) + || $this->not && !preg_match($this->pattern, $value); + } + + /** * Returns the JavaScript needed for performing client-side validation. * @param \yii\base\Model $object the data object being validated * @param string $attribute the name of the attribute to be validated. * @return string the client-side validation script. - * @throws \yii\base\Exception if the "pattern" is not a valid regular expression + * @throws InvalidConfigException if the "pattern" is not a valid regular expression */ public function clientValidateAttribute($object, $attribute) { - if ($this->pattern === null) { - throw new \yii\base\Exception('The "pattern" property must be specified with a valid regular expression.'); - } - $message = ($this->message !== null) ? $this->message : \Yii::t('yii|{attribute} is invalid.'); $message = strtr($message, array( '{attribute}' => $object->getAttributeLabel($attribute), diff --git a/framework/validators/RequiredValidator.php b/framework/validators/RequiredValidator.php index 66b9c3c..944c695 100644 --- a/framework/validators/RequiredValidator.php +++ b/framework/validators/RequiredValidator.php @@ -16,6 +16,10 @@ namespace yii\validators; class RequiredValidator extends Validator { /** + * @var boolean whether to skip this validator if the value being validated is empty. + */ + public $skipOnEmpty = false; + /** * @var mixed the desired value that the attribute must have. * If this is null, the validator will validate that the specified attribute is not empty. * If this is set as a value that is not null, the validator will validate that @@ -59,6 +63,23 @@ class RequiredValidator extends Validator } /** + * Validates the given value. + * @param mixed $value the value to be validated. + * @return boolean whether the value is valid. + */ + public function validateValue($value) + { + if ($this->requiredValue === null) { + if ($this->strict && $value !== null || !$this->strict && !$this->isEmpty($value, true)) { + return true; + } + } elseif (!$this->strict && $value == $this->requiredValue || $this->strict && $value === $this->requiredValue) { + return true; + } + return false; + } + + /** * Returns the JavaScript needed for performing client-side validation. * @param \yii\base\Model $object the data object being validated * @param string $attribute the name of the attribute to be validated. diff --git a/framework/validators/StringValidator.php b/framework/validators/StringValidator.php index 9135b9e..83ff35b 100644 --- a/framework/validators/StringValidator.php +++ b/framework/validators/StringValidator.php @@ -7,6 +7,8 @@ namespace yii\validators; +use Yii; + /** * StringValidator validates that the attribute value is of certain length. * @@ -46,19 +48,22 @@ class StringValidator extends Validator */ public $notEqual; /** - * @var boolean whether the attribute value can be null or empty. Defaults to true, - * meaning that if the attribute is empty, it is considered valid. + * @var string the encoding of the string value to be validated (e.g. 'UTF-8'). + * If this property is not set, [[\yii\base\Application::charset]] will be used. */ - public $allowEmpty = true; + public $encoding; + + /** - * @var mixed the encoding of the string value to be validated (e.g. 'UTF-8'). - * This property is used only when mbstring PHP extension is enabled. - * The value of this property will be used as the 2nd parameter of the - * mb_strlen() function. If this property is not set, the application charset - * will be used. If this property is set false, then strlen() will be used even - * if mbstring is enabled. + * Initializes the validator. */ - public $encoding; + public function init() + { + parent::init(); + if ($this->encoding === null) { + $this->encoding = Yii::$app->charset; + } + } /** * Validates the attribute of the object. @@ -69,37 +74,46 @@ class StringValidator extends Validator public function validateAttribute($object, $attribute) { $value = $object->$attribute; - if ($this->allowEmpty && $this->isEmpty($value)) { - return; - } if (!is_string($value)) { - $message = ($this->message !== null) ? $this->message : \Yii::t('yii|{attribute} must be a string.'); + $message = ($this->message !== null) ? $this->message : Yii::t('yii|{attribute} must be a string.'); $this->addError($object, $attribute, $message); return; } - if (function_exists('mb_strlen') && $this->encoding !== false) { - $length = mb_strlen($value, $this->encoding ? $this->encoding : \Yii::$app->charset); - } else { - $length = strlen($value); - } + $length = mb_strlen($value, $this->encoding); if ($this->min !== null && $length < $this->min) { - $message = ($this->tooShort !== null) ? $this->tooShort : \Yii::t('yii|{attribute} is too short (minimum is {min} characters).'); + $message = ($this->tooShort !== null) ? $this->tooShort : Yii::t('yii|{attribute} should contain at least {min} characters.'); $this->addError($object, $attribute, $message, array('{min}' => $this->min)); } if ($this->max !== null && $length > $this->max) { - $message = ($this->tooLong !== null) ? $this->tooLong : \Yii::t('yii|{attribute} is too long (maximum is {max} characters).'); + $message = ($this->tooLong !== null) ? $this->tooLong : Yii::t('yii|{attribute} should contain at most {max} characters.'); $this->addError($object, $attribute, $message, array('{max}' => $this->max)); } if ($this->is !== null && $length !== $this->is) { - $message = ($this->notEqual !== null) ? $this->notEqual : \Yii::t('yii|{attribute} is of the wrong length (should be {length} characters).'); + $message = ($this->notEqual !== null) ? $this->notEqual : Yii::t('yii|{attribute} should contain {length} characters.'); $this->addError($object, $attribute, $message, array('{length}' => $this->is)); } } /** + * Validates the given value. + * @param mixed $value the value to be validated. + * @return boolean whether the value is valid. + */ + public function validateValue($value) + { + if (!is_string($value)) { + return false; + } + $length = mb_strlen($value, $this->encoding); + return ($this->min === null || $length >= $this->min) + && ($this->max === null || $length <= $this->max) + && ($this->is === null || $length === $this->is); + } + + /** * Returns the JavaScript needed for performing client-side validation. * @param \yii\base\Model $object the data object being validated * @param string $attribute the name of the attribute to be validated. @@ -111,7 +125,7 @@ class StringValidator extends Validator $value = $object->$attribute; if (($notEqual = $this->notEqual) === null) { - $notEqual = \Yii::t('yii|{attribute} is of the wrong length (should be {length} characters).'); + $notEqual = Yii::t('yii|{attribute} should contain {length} characters.'); } $notEqual = strtr($notEqual, array( '{attribute}' => $label, @@ -120,7 +134,7 @@ class StringValidator extends Validator )); if (($tooShort = $this->tooShort) === null) { - $tooShort = \Yii::t('yii|{attribute} is too short (minimum is {min} characters).'); + $tooShort = Yii::t('yii|{attribute} should contain at least {min} characters.'); } $tooShort = strtr($tooShort, array( '{attribute}' => $label, @@ -129,7 +143,7 @@ class StringValidator extends Validator )); if (($tooLong = $this->tooLong) === null) { - $tooLong = \Yii::t('yii|{attribute} is too long (maximum is {max} characters).'); + $tooLong = Yii::t('yii|{attribute} should contain at most {max} characters.'); } $tooLong = strtr($tooLong, array( '{attribute}' => $label, diff --git a/framework/validators/UniqueValidator.php b/framework/validators/UniqueValidator.php index bc12f5a..8d5c8b7 100644 --- a/framework/validators/UniqueValidator.php +++ b/framework/validators/UniqueValidator.php @@ -17,11 +17,6 @@ use yii\base\InvalidConfigException; class UniqueValidator extends Validator { /** - * @var boolean whether the attribute value can be null or empty. Defaults to true, - * meaning that if the attribute is empty, it is considered valid. - */ - public $allowEmpty = true; - /** * @var string the ActiveRecord class name or alias of the class * that should be used to look for the attribute value being validated. * Defaults to null, meaning using the ActiveRecord class of the attribute being validated. @@ -45,10 +40,6 @@ class UniqueValidator extends Validator public function validateAttribute($object, $attribute) { $value = $object->$attribute; - if ($this->allowEmpty && $this->isEmpty($value)) { - return; - } - /** @var $className \yii\db\ActiveRecord */ $className = $this->className === null ? get_class($object) : \Yii::import($this->className); $attributeName = $this->attributeName === null ? $attribute : $this->attributeName; diff --git a/framework/validators/UrlValidator.php b/framework/validators/UrlValidator.php index 0ba039b..fb743e0 100644 --- a/framework/validators/UrlValidator.php +++ b/framework/validators/UrlValidator.php @@ -32,11 +32,6 @@ class UrlValidator extends Validator * contain the scheme part. **/ public $defaultScheme; - /** - * @var boolean whether the attribute value can be null or empty. Defaults to true, - * meaning that if the attribute is empty, it is considered valid. - */ - public $allowEmpty = true; /** * Validates the attribute of the object. @@ -47,11 +42,10 @@ class UrlValidator extends Validator public function validateAttribute($object, $attribute) { $value = $object->$attribute; - if ($this->allowEmpty && $this->isEmpty($value)) { - return; - } - if (($value = $this->validateValue($value)) !== false) { - $object->$attribute = $value; + if ($this->validateValue($value)) { + if ($this->defaultScheme !== null && strpos($value, '://') === false) { + $object->$attribute = $this->defaultScheme . '://' . $value; + } } else { $message = ($this->message !== null) ? $this->message : \Yii::t('yii|{attribute} is not a valid URL.'); $this->addError($object, $attribute, $message); @@ -59,11 +53,9 @@ class UrlValidator extends Validator } /** - * Validates a static value to see if it is a valid URL. - * Note that this method does not respect [[allowEmpty]] property. - * This method is provided so that you can call it directly without going through the model validation rule mechanism. - * @param mixed $value the value to be validated - * @return mixed false if the the value is not a valid URL, otherwise the possibly modified value ({@see defaultScheme}) + * Validates the given value. + * @param mixed $value the value to be validated. + * @return boolean whether the value is valid. */ public function validateValue($value) { @@ -80,7 +72,7 @@ class UrlValidator extends Validator } if (preg_match($pattern, $value)) { - return $value; + return true; } } return false; diff --git a/framework/validators/Validator.php b/framework/validators/Validator.php index 00a88ba..4dc58ae 100644 --- a/framework/validators/Validator.php +++ b/framework/validators/Validator.php @@ -7,6 +7,7 @@ namespace yii\validators; +use Yii; use yii\base\Component; use yii\base\NotSupportedException; @@ -95,6 +96,12 @@ abstract class Validator extends Component */ public $skipOnError = true; /** + * @var boolean whether this validation rule should be skipped if the attribute value + * is null or an empty string. + */ + public $skipOnEmpty = true; + + /** * @var boolean whether to enable client-side validation. Defaults to null, meaning * its actual value inherits from that of [[\yii\web\ActiveForm::enableClientValidation]]. */ @@ -150,7 +157,7 @@ abstract class Validator extends Component } } - return \Yii::createObject($params); + return Yii::createObject($params); } /** @@ -169,12 +176,20 @@ abstract class Validator extends Component $attributes = $this->attributes; } foreach ($attributes as $attribute) { - if (!($this->skipOnError && $object->hasErrors($attribute))) { + $skip = $this->skipOnError && $object->hasErrors($attribute) + || $this->skipOnEmpty && $this->isEmpty($object->$attribute); + if (!$skip) { $this->validateAttribute($object, $attribute); } } } + /** + * Validates a value. + * A validator class can implement this method to support data validation out of the context of a data model. + * @param mixed $value the data value to be validated. + * @throws NotSupportedException if data validation without a model is not supported + */ public function validateValue($value) { throw new NotSupportedException(__CLASS__ . ' does not support validateValue().'); From 421e31ec0fcf304a93cd79401b5b0798225158ad Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Thu, 4 Apr 2013 10:57:01 -0400 Subject: [PATCH 030/104] finished validator refactoring. --- framework/validators/CaptchaValidator.php | 2 +- framework/validators/ExistValidator.php | 8 ++++++++ framework/validators/NumberValidator.php | 4 ++++ framework/validators/RegularExpressionValidator.php | 9 +++++---- framework/validators/UniqueValidator.php | 6 ++++++ framework/validators/Validator.php | 2 +- 6 files changed, 25 insertions(+), 6 deletions(-) diff --git a/framework/validators/CaptchaValidator.php b/framework/validators/CaptchaValidator.php index f35b332..65e7fd3 100644 --- a/framework/validators/CaptchaValidator.php +++ b/framework/validators/CaptchaValidator.php @@ -53,7 +53,7 @@ class CaptchaValidator extends Validator public function validateValue($value) { $captcha = $this->getCaptchaAction(); - return $captcha->validate($value, $this->caseSensitive); + return !is_array($value) && $captcha->validate($value, $this->caseSensitive); } /** diff --git a/framework/validators/ExistValidator.php b/framework/validators/ExistValidator.php index b39be56..ec01134 100644 --- a/framework/validators/ExistValidator.php +++ b/framework/validators/ExistValidator.php @@ -48,6 +48,11 @@ class ExistValidator extends Validator { $value = $object->$attribute; + if (is_array($value)) { + $this->addError($object, $attribute, Yii::t('yii|{attribute} is invalid.')); + return; + } + /** @var $className \yii\db\ActiveRecord */ $className = $this->className === null ? get_class($object) : Yii::import($this->className); $attributeName = $this->attributeName === null ? $attribute : $this->attributeName; @@ -67,6 +72,9 @@ class ExistValidator extends Validator */ public function validateValue($value) { + if (is_array($value)) { + return false; + } if ($this->className === null) { throw new InvalidConfigException('The "className" property must be set.'); } diff --git a/framework/validators/NumberValidator.php b/framework/validators/NumberValidator.php index 6219bdb..4d7297f 100644 --- a/framework/validators/NumberValidator.php +++ b/framework/validators/NumberValidator.php @@ -61,6 +61,10 @@ class NumberValidator extends Validator public function validateAttribute($object, $attribute) { $value = $object->$attribute; + if (is_array($value)) { + $this->addError($object, $attribute, Yii::t('yii|{attribute} is invalid.')); + return; + } if ($this->integerOnly) { if (!preg_match($this->integerPattern, "$value")) { $message = $this->message !== null ? $this->message : Yii::t('yii|{attribute} must be an integer.'); diff --git a/framework/validators/RegularExpressionValidator.php b/framework/validators/RegularExpressionValidator.php index b0811e9..d88f613 100644 --- a/framework/validators/RegularExpressionValidator.php +++ b/framework/validators/RegularExpressionValidator.php @@ -51,8 +51,8 @@ class RegularExpressionValidator extends Validator public function validateAttribute($object, $attribute) { $value = $object->$attribute; - if ((!$this->not && !preg_match($this->pattern, $value)) || ($this->not && preg_match($this->pattern, $value))) { - $message = ($this->message !== null) ? $this->message : \Yii::t('yii|{attribute} is invalid.'); + if (!$this->validateValue($value)) { + $message = $this->message !== null ? $this->message : \Yii::t('yii|{attribute} is invalid.'); $this->addError($object, $attribute, $message); } } @@ -64,8 +64,9 @@ class RegularExpressionValidator extends Validator */ public function validateValue($value) { - return !$this->not && preg_match($this->pattern, $value) - || $this->not && !preg_match($this->pattern, $value); + return !is_array($value) && + (!$this->not && preg_match($this->pattern, $value) + || $this->not && !preg_match($this->pattern, $value)); } /** diff --git a/framework/validators/UniqueValidator.php b/framework/validators/UniqueValidator.php index 8d5c8b7..30735b1 100644 --- a/framework/validators/UniqueValidator.php +++ b/framework/validators/UniqueValidator.php @@ -40,6 +40,12 @@ class UniqueValidator extends Validator public function validateAttribute($object, $attribute) { $value = $object->$attribute; + + if (is_array($value)) { + $this->addError($object, $attribute, Yii::t('yii|{attribute} is invalid.')); + return; + } + /** @var $className \yii\db\ActiveRecord */ $className = $this->className === null ? get_class($object) : \Yii::import($this->className); $attributeName = $this->attributeName === null ? $attribute : $this->attributeName; diff --git a/framework/validators/Validator.php b/framework/validators/Validator.php index 4dc58ae..b75f86e 100644 --- a/framework/validators/Validator.php +++ b/framework/validators/Validator.php @@ -192,7 +192,7 @@ abstract class Validator extends Component */ public function validateValue($value) { - throw new NotSupportedException(__CLASS__ . ' does not support validateValue().'); + throw new NotSupportedException(get_class($this) . ' does not support validateValue().'); } /** From bb5b6a4191e849483692f93011e147b4b8ae304d Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Thu, 4 Apr 2013 16:53:50 -0400 Subject: [PATCH 031/104] Finished FileValidator and UploadedFile. Added Model::formName(). --- framework/base/Model.php | 22 ++- framework/validators/FileValidator.php | 283 ++++++++++++++++++--------------- framework/web/UploadedFile.php | 246 ++++++++++++++++++++++++++++ framework/widgets/ActiveForm.php | 63 ++++---- 4 files changed, 450 insertions(+), 164 deletions(-) create mode 100644 framework/web/UploadedFile.php diff --git a/framework/base/Model.php b/framework/base/Model.php index 611b12e..7f55239 100644 --- a/framework/base/Model.php +++ b/framework/base/Model.php @@ -8,8 +8,8 @@ namespace yii\base; use yii\helpers\StringHelper; -use yii\validators\Validator; use yii\validators\RequiredValidator; +use yii\validators\Validator; /** * Model is the base class for data models. @@ -169,6 +169,26 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess } /** + * Returns the form name that this model class should use. + * + * The form name is mainly used by [[\yii\web\ActiveForm]] to determine how to name + * the input fields for the attributes in a model. If the form name is "A" and an attribute + * name is "b", then the corresponding input name would be "A[b]". If the form name is + * an empty string, then the input name would be "b". + * + * By default, this method returns the model class name (without the namespace part) + * as the form name. You may override it when the model is used in different forms. + * + * @return string the form name of this model class. + */ + public function formName() + { + $class = get_class($this); + $pos = strrpos($class, '\\'); + return $pos === false ? $class : substr($class, $pos + 1); + } + + /** * Returns the list of attribute names. * By default, this method returns all public non-static properties of the class. * You may override this method to change the default behavior. diff --git a/framework/validators/FileValidator.php b/framework/validators/FileValidator.php index b05ac2a..c104c05 100644 --- a/framework/validators/FileValidator.php +++ b/framework/validators/FileValidator.php @@ -7,47 +7,19 @@ namespace yii\validators; +use Yii; +use yii\helpers\FileHelper; +use yii\web\UploadedFile; + /** - * CFileValidator verifies if an attribute is receiving a valid uploaded file. - * - * It uses the model class and attribute name to retrieve the information - * about the uploaded file. It then checks if a file is uploaded successfully, - * if the file size is within the limit and if the file type is allowed. - * - * This validator will attempt to fetch uploaded data if attribute is not - * previously set. Please note that this cannot be done if input is tabular: - *

    - *  foreach($models as $i=>$model)
    - *     $model->attribute = CUploadedFile::getInstance($model, "[$i]attribute");
    - * 
    - * Please note that you must use {@link CUploadedFile::getInstances} for multiple - * file uploads. - * - * When using CFileValidator with an active record, the following code is often used: - *
    - *  if($model->save())
    - *  {
    - *     // single upload
    - *     $model->attribute->saveAs($path);
    - *     // multiple upload
    - *     foreach($model->attribute as $file)
    - *        $file->saveAs($path);
    - *  }
    - * 
    - * - * You can use {@link CFileValidator} to validate the file attribute. + * FileValidator verifies if an attribute is receiving a valid uploaded file. * * @author Qiang Xue * @since 2.0 */ -class CFileValidator extends Validator +class FileValidator extends Validator { /** - * @var boolean whether the attribute requires a file to be uploaded or not. - * Defaults to false, meaning a file is required to be uploaded. - */ - public $allowEmpty = false; - /** * @var mixed a list of file name extensions that are allowed to be uploaded. * This can be either an array or a string consisting of file extension names * separated by space or comma (e.g. "gif, jpg"). @@ -66,136 +38,179 @@ class CFileValidator extends Validator * Defaults to null, meaning no limit. * Note, the size limit is also affected by 'upload_max_filesize' INI setting * and the 'MAX_FILE_SIZE' hidden field value. - * @see tooLarge + * @see tooBig */ public $maxSize; /** + * @var integer the maximum file count the given attribute can hold. + * It defaults to 1, meaning single file upload. By defining a higher number, + * multiple uploads become possible. + */ + public $maxFiles = 1; + /** + * @var string the error message used when a file is not uploaded correctly. + */ + public $message; + /** + * @var string the error message used when no file is uploaded. + */ + public $uploadRequired; + /** * @var string the error message used when the uploaded file is too large. - * @see maxSize + * You may use the following tokens in the message: + * + * - {attribute}: the attribute name + * - {file}: the uploaded file name + * - {limit}: the maximum size allowed (see [[getSizeLimit()]]) */ - public $tooLarge; + public $tooBig; /** * @var string the error message used when the uploaded file is too small. - * @see minSize + * You may use the following tokens in the message: + * + * - {attribute}: the attribute name + * - {file}: the uploaded file name + * - {limit}: the value of [[minSize]] */ public $tooSmall; /** * @var string the error message used when the uploaded file has an extension name - * that is not listed among {@link extensions}. + * that is not listed in [[extensions]]. You may use the following tokens in the message: + * + * - {attribute}: the attribute name + * - {extensions}: the list of the allowed extensions. */ public $wrongType; /** - * @var integer the maximum file count the given attribute can hold. - * It defaults to 1, meaning single file upload. By defining a higher number, - * multiple uploads become possible. - */ - public $maxFiles = 1; - /** - * @var string the error message used if the count of multiple uploads exceeds - * limit. + * @var string the error message used if the count of multiple uploads exceeds limit. + * You may use the following tokens in the message: + * + * - {attribute}: the attribute name + * - {file}: the uploaded file name + * - {limit}: the value of [[maxFiles]] */ public $tooMany; /** - * Set the attribute and then validates using {@link validateFile}. - * If there is any error, the error message is added to the object. - * @param \yii\base\Model $object the object being validated - * @param string $attribute the attribute being validated + * Initializes the validator. */ - public function validateAttribute($object, $attribute) + public function init() { - if ($this->maxFiles > 1) - { - $files = $object->$attribute; - if (!is_array($files) || !isset($files[0]) || !$files[0] instanceof CUploadedFile) - $files = CUploadedFile::getInstances($object, $attribute); - if (array() === $files) - return $this->emptyAttribute($object, $attribute); - if (count($files) > $this->maxFiles) - { - $message = $this->tooMany !== null ? $this->tooMany : \Yii::t('yii|{attribute} cannot accept more than {limit} files.'); - $this->addError($object, $attribute, $message, array('{attribute}' => $attribute, '{limit}' => $this->maxFiles)); - } else - foreach ($files as $file) - $this->validateFile($object, $attribute, $file); - } else - { - $file = $object->$attribute; - if (!$file instanceof CUploadedFile) - { - $file = CUploadedFile::getInstance($object, $attribute); - if (null === $file) - return $this->emptyAttribute($object, $attribute); - } - $this->validateFile($object, $attribute, $file); + parent::init(); + if ($this->message === null) { + $this->message = Yii::t('yii|File upload failed.'); + } + if ($this->uploadRequired === null) { + $this->uploadRequired = Yii::t('yii|Please upload a file.'); } + if ($this->tooMany === null) { + $this->tooMany = Yii::t('yii|You can upload at most {limit} files.'); + } + if ($this->wrongType === null) { + $this->wrongType = Yii::t('yii|Only files with these extensions are allowed: {extensions}.'); + } + if ($this->tooBig === null) { + $this->tooBig = Yii::t('yii|The file "{file}" is too big. Its size cannot exceed {limit} bytes.'); + } + if ($this->tooSmall === null) { + $this->tooSmall = Yii::t('yii|The file "{file}" is too small. Its size cannot be smaller than {limit} bytes.'); + } + if (!is_array($this->types)) { + $this->types = preg_split('/[\s,]+/', strtolower($this->types), -1, PREG_SPLIT_NO_EMPTY); + } } /** - * Internally validates a file object. + * Validates the attribute. * @param \yii\base\Model $object the object being validated * @param string $attribute the attribute being validated - * @param CUploadedFile $file uploaded file passed to check against a set of rules */ - public function validateFile($object, $attribute, $file) + public function validateAttribute($object, $attribute) { - if (null === $file || ($error = $file->getError()) == UPLOAD_ERR_NO_FILE) - return $this->emptyAttribute($object, $attribute); - elseif ($error == UPLOAD_ERR_INI_SIZE || $error == UPLOAD_ERR_FORM_SIZE || $this->maxSize !== null && $file->getSize() > $this->maxSize) - { - $message = $this->tooLarge !== null ? $this->tooLarge : \Yii::t('yii|The file "{file}" is too large. Its size cannot exceed {limit} bytes.'); - $this->addError($object, $attribute, $message, array('{file}' => $file->getName(), '{limit}' => $this->getSizeLimit())); - } elseif ($error == UPLOAD_ERR_PARTIAL) - throw new CException(\Yii::t('yii|The file "{file}" was only partially uploaded.', array('{file}' => $file->getName()))); - elseif ($error == UPLOAD_ERR_NO_TMP_DIR) - throw new CException(\Yii::t('yii|Missing the temporary folder to store the uploaded file "{file}".', array('{file}' => $file->getName()))); - elseif ($error == UPLOAD_ERR_CANT_WRITE) - throw new CException(\Yii::t('yii|Failed to write the uploaded file "{file}" to disk.', array('{file}' => $file->getName()))); - elseif (defined('UPLOAD_ERR_EXTENSION') && $error == UPLOAD_ERR_EXTENSION) // available for PHP 5.2.0 or above - throw new CException(\Yii::t('yii|File upload was stopped by extension.')); - - if ($this->minSize !== null && $file->getSize() < $this->minSize) - { - $message = $this->tooSmall !== null ? $this->tooSmall : \Yii::t('yii|The file "{file}" is too small. Its size cannot be smaller than {limit} bytes.'); - $this->addError($object, $attribute, $message, array('{file}' => $file->getName(), '{limit}' => $this->minSize)); - } - - if ($this->types !== null) - { - if (is_string($this->types)) - $types = preg_split('/[\s,]+/', strtolower($this->types), -1, PREG_SPLIT_NO_EMPTY); - else - $types = $this->types; - if (!in_array(strtolower($file->getExtensionName()), $types)) - { - $message = $this->wrongType !== null ? $this->wrongType : \Yii::t('yii|The file "{file}" cannot be uploaded. Only files with these extensions are allowed: {extensions}.'); - $this->addError($object, $attribute, $message, array('{file}' => $file->getName(), '{extensions}' => implode(', ', $types))); + if ($this->maxFiles > 1) { + $files = $object->$attribute; + if (!is_array($files)) { + $this->addError($object, $attribute, $this->uploadRequired); + return; + } + foreach ($files as $i => $file) { + if (!$file instanceof UploadedFile || $file->getError() == UPLOAD_ERR_NO_FILE) { + unset($files[$i]); + } + } + $object->$attribute = array_values($files); + if ($files === array()) { + $this->addError($object, $attribute, $this->uploadRequired); + } + if (count($files) > $this->maxFiles) { + $this->addError($object, $attribute, $this->tooMany, array('{attribute}' => $attribute, '{limit}' => $this->maxFiles)); + } else { + foreach ($files as $file) { + $this->validateFile($object, $attribute, $file); + } + } + } else { + $file = $object->$attribute; + if ($file instanceof UploadedFile && $file->getError() != UPLOAD_ERR_NO_FILE) { + $this->validateFile($object, $attribute, $file); + } else { + $this->addError($object, $attribute, $this->uploadRequired); } } } /** - * Raises an error to inform end user about blank attribute. + * Internally validates a file object. * @param \yii\base\Model $object the object being validated * @param string $attribute the attribute being validated + * @param UploadedFile $file uploaded file passed to check against a set of rules */ - public function emptyAttribute($object, $attribute) + protected function validateFile($object, $attribute, $file) { - if (!$this->allowEmpty) - { - $message = $this->message !== null ? $this->message : \Yii::t('yii|{attribute} cannot be blank.'); - $this->addError($object, $attribute, $message); + switch ($file->getError()) { + case UPLOAD_ERR_OK: + if ($this->maxSize !== null && $file->getSize() > $this->maxSize) { + $this->addError($object, $attribute, $this->tooBig, array('{file}' => $file->getName(), '{limit}' => $this->getSizeLimit())); + } + if ($this->minSize !== null && $file->getSize() < $this->minSize) { + $this->addError($object, $attribute, $this->tooSmall, array('{file}' => $file->getName(), '{limit}' => $this->minSize)); + } + if (!empty($this->types) && !in_array(strtolower(FileHelper::getExtension($file->getName())), $this->types, true)) { + $this->addError($object, $attribute, $this->wrongType, array('{file}' => $file->getName(), '{extensions}' => implode(', ', $this->types))); + } + break; + case UPLOAD_ERR_INI_SIZE: + case UPLOAD_ERR_FORM_SIZE: + $this->addError($object, $attribute, $this->tooBig, array('{file}' => $file->getName(), '{limit}' => $this->getSizeLimit())); + break; + case UPLOAD_ERR_PARTIAL: + $this->addError($object, $attribute, $this->message); + Yii::warning('File was only partially uploaded: ' . $file->getName(), __METHOD__); + break; + case UPLOAD_ERR_NO_TMP_DIR: + $this->addError($object, $attribute, $this->message); + Yii::warning('Missing the temporary folder to store the uploaded file: ' . $file->getName(), __METHOD__); + break; + case UPLOAD_ERR_CANT_WRITE: + $this->addError($object, $attribute, $this->message); + Yii::warning('Failed to write the uploaded file to disk: ', $file->getName(), __METHOD__); + break; + case UPLOAD_ERR_EXTENSION: + $this->addError($object, $attribute, $this->message); + Yii::warning('File upload was stopped by some PHP extension: ', $file->getName(), __METHOD__); + break; + default: + break; } } /** * Returns the maximum size allowed for uploaded files. * This is determined based on three factors: - *
      - *
    • 'upload_max_filesize' in php.ini
    • - *
    • 'MAX_FILE_SIZE' hidden field
    • - *
    • {@link maxSize}
    • - *
    + * + * - 'upload_max_filesize' in php.ini + * - 'MAX_FILE_SIZE' hidden field + * - [[maxSize]] * * @return integer the size limit for uploaded files. */ @@ -203,10 +218,12 @@ class CFileValidator extends Validator { $limit = ini_get('upload_max_filesize'); $limit = $this->sizeToBytes($limit); - if ($this->maxSize !== null && $limit > 0 && $this->maxSize < $limit) + if ($this->maxSize !== null && $limit > 0 && $this->maxSize < $limit) { $limit = $this->maxSize; - if (isset($_POST['MAX_FILE_SIZE']) && $_POST['MAX_FILE_SIZE'] > 0 && $_POST['MAX_FILE_SIZE'] < $limit) - $limit = $_POST['MAX_FILE_SIZE']; + } + if (isset($_POST['MAX_FILE_SIZE']) && $_POST['MAX_FILE_SIZE'] > 0 && $_POST['MAX_FILE_SIZE'] < $limit) { + $limit = (int)$_POST['MAX_FILE_SIZE']; + } return $limit; } @@ -218,12 +235,18 @@ class CFileValidator extends Validator */ private function sizeToBytes($sizeStr) { - switch (substr($sizeStr, -1)) - { - case 'M': case 'm': return (int)$sizeStr * 1048576; - case 'K': case 'k': return (int)$sizeStr * 1024; - case 'G': case 'g': return (int)$sizeStr * 1073741824; - default: return (int)$sizeStr; + switch (substr($sizeStr, -1)) { + case 'M': + case 'm': + return (int)$sizeStr * 1048576; + case 'K': + case 'k': + return (int)$sizeStr * 1024; + case 'G': + case 'g': + return (int)$sizeStr * 1073741824; + default: + return (int)$sizeStr; } } } \ No newline at end of file diff --git a/framework/web/UploadedFile.php b/framework/web/UploadedFile.php new file mode 100644 index 0000000..c67281c --- /dev/null +++ b/framework/web/UploadedFile.php @@ -0,0 +1,246 @@ + + * @since 2.0 + */ +class UploadedFile extends \yii\base\Object +{ + private static $_files; + private $_name; + private $_tempName; + private $_type; + private $_size; + private $_error; + + + /** + * Constructor. + * Instead of using the constructor to create a new instance, + * you should normally call [[getInstance()]] or [[getInstances()]] + * to obtain new instances. + * @param string $name the original name of the file being uploaded + * @param string $tempName the path of the uploaded file on the server. + * @param string $type the MIME-type of the uploaded file (such as "image/gif"). + * @param integer $size the actual size of the uploaded file in bytes + * @param integer $error the error code + */ + public function __construct($name, $tempName, $type, $size, $error) + { + $this->_name = $name; + $this->_tempName = $tempName; + $this->_type = $type; + $this->_size = $size; + $this->_error = $error; + } + + /** + * String output. + * This is PHP magic method that returns string representation of an object. + * The implementation here returns the uploaded file's name. + * @return string the string representation of the object + */ + public function __toString() + { + return $this->_name; + } + + /** + * Returns an uploaded file for the given model attribute. + * The file should be uploaded using [[ActiveForm::fileInput()]]. + * @param \yii\base\Model $model the data model + * @param string $attribute the attribute name. The attribute name may contain array indexes. + * For example, '[1]file' for tabular file uploading; and 'file[1]' for an element in a file array. + * @return UploadedFile the instance of the uploaded file. + * Null is returned if no file is uploaded for the specified model attribute. + * @see getInstanceByName + */ + public static function getInstance($model, $attribute) + { + $name = ActiveForm::getInputName($model, $attribute); + return static::getInstanceByName($name); + } + + /** + * Returns all uploaded files for the given model attribute. + * @param \yii\base\Model $model the data model + * @param string $attribute the attribute name. The attribute name may contain array indexes + * for tabular file uploading, e.g. '[1]file'. + * @return UploadedFile[] array of UploadedFile objects. + * Empty array is returned if no available file was found for the given attribute. + */ + public static function getInstances($model, $attribute) + { + $name = ActiveForm::getInputName($model, $attribute); + return static::getInstancesByName($name); + } + + /** + * Returns an uploaded file according to the given file input name. + * The name can be a plain string or a string like an array element (e.g. 'Post[imageFile]', or 'Post[0][imageFile]'). + * @param string $name the name of the file input field. + * @return UploadedFile the instance of the uploaded file. + * Null is returned if no file is uploaded for the specified name. + */ + public static function getInstanceByName($name) + { + $files = static::loadFiles(); + return isset($files[$name]) ? $files[$name] : null; + } + + /** + * Returns an array of uploaded files corresponding to the specified file input name. + * This is mainly used when multiple files were uploaded and saved as 'files[0]', 'files[1]', + * 'files[n]'..., and you can retrieve them all by passing 'files' as the name. + * @param string $name the name of the array of files + * @return UploadedFile[] the array of CUploadedFile objects. Empty array is returned + * if no adequate upload was found. Please note that this array will contain + * all files from all sub-arrays regardless how deeply nested they are. + */ + public static function getInstancesByName($name) + { + $files = static::loadFiles(); + if (isset($files[$name])) { + return array($files[$name]); + } + $results = array(); + foreach ($files as $key => $file) { + if (strpos($key, "{$name}[") === 0) { + $results[] = self::$_files[$key]; + } + } + return $results; + } + + /** + * Cleans up the loaded UploadedFile instances. + * This method is mainly used by test scripts to set up a fixture. + */ + public static function reset() + { + self::$_files = null; + } + + /** + * Saves the uploaded file. + * Note that this method uses php's move_uploaded_file() method. If the target file `$file` + * already exists, it will be overwritten. + * @param string $file the file path used to save the uploaded file + * @param boolean $deleteTempFile whether to delete the temporary file after saving. + * If true, you will not be able to save the uploaded file again in the current request. + * @return boolean true whether the file is saved successfully + * @see error + */ + public function saveAs($file, $deleteTempFile = true) + { + if ($this->_error == UPLOAD_ERR_OK) { + if ($deleteTempFile) { + return move_uploaded_file($this->_tempName, $file); + } elseif (is_uploaded_file($this->_tempName)) { + return copy($this->_tempName, $file); + } + } + return false; + } + + /** + * @return string the original name of the file being uploaded + */ + public function getName() + { + return $this->_name; + } + + /** + * @return string the path of the uploaded file on the server. + * Note, this is a temporary file which will be automatically deleted by PHP + * after the current request is processed. + */ + public function getTempName() + { + return $this->_tempName; + } + + /** + * @return string the MIME-type of the uploaded file (such as "image/gif"). + * Since this MIME type is not checked on the server side, do not take this value for granted. + * Instead, use [[FileHelper::getMimeType()]] to determine the exact MIME type. + */ + public function getType() + { + return $this->_type; + } + + /** + * @return integer the actual size of the uploaded file in bytes + */ + public function getSize() + { + return $this->_size; + } + + /** + * Returns an error code describing the status of this file uploading. + * @return integer the error code + * @see http://www.php.net/manual/en/features.file-upload.errors.php + */ + public function getError() + { + return $this->_error; + } + + /** + * @return boolean whether there is an error with the uploaded file. + * Check [[error]] for detailed error code information. + */ + public function getHasError() + { + return $this->_error != UPLOAD_ERR_OK; + } + + /** + * Creates UploadedFile instances from $_FILE. + * @return array the UploadedFile instances + */ + private static function loadFiles() + { + if (self::$_files === null) { + self::$_files = array(); + if (isset($_FILES) && is_array($_FILES)) { + foreach ($_FILES as $class => $info) { + self::loadFilesRecursive($class, $info['name'], $info['tmp_name'], $info['type'], $info['size'], $info['error']); + } + } + } + return self::$_files; + } + + /** + * Creates UploadedFile instances from $_FILE recursively. + * @param string $key key for identifying uploaded file: class name and sub-array indexes + * @param mixed $names file names provided by PHP + * @param mixed $tempNames temporary file names provided by PHP + * @param mixed $types file types provided by PHP + * @param mixed $sizes file sizes provided by PHP + * @param mixed $errors uploading issues provided by PHP + */ + private static function loadFilesRecursive($key, $names, $tempNames, $types, $sizes, $errors) + { + if (is_array($names)) { + foreach ($names as $i => $name) { + self::loadFilesRecursive($key . '[' . $i . ']', $name, $tempNames[$i], $types[$i], $sizes[$i], $errors[$i]); + } + } else { + self::$_files[$key] = new self($names, $tempNames, $types, $sizes, $errors); + } + } +} diff --git a/framework/widgets/ActiveForm.php b/framework/widgets/ActiveForm.php index 8ac5365..48bc181 100644 --- a/framework/widgets/ActiveForm.php +++ b/framework/widgets/ActiveForm.php @@ -52,10 +52,6 @@ class ActiveForm extends Widget public $enableClientValidation = false; public $options = array(); - /** - * @var array model-class mapped to name prefix - */ - public $modelMap; /** * @param Model|Model[] $models @@ -240,35 +236,6 @@ class ActiveForm extends Widget 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 + 1); - } - 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 getInputId($model, $attribute) - { - $name = $this->getInputName($model, $attribute); - return str_replace(array('[]', '][', '[', ']', ' '), array('', '-', '-', '', '-'), $name); - } - public function getAttributeValue($model, $attribute) { if (!preg_match('/(^|.*\])(\w+)(\[.*|$)/', $attribute, $matches)) { @@ -299,4 +266,34 @@ class ActiveForm extends Widget throw new InvalidParamException('Attribute name must contain word characters only.'); } } + + /** + * @param Model $model + * @param string $attribute + * @return string + * @throws \yii\base\InvalidParamException + */ + public static function getInputName($model, $attribute) + { + $formName = $model->formName(); + 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 ($formName === '' && $prefix === '') { + return $attribute . $suffix; + } elseif ($formName !== '') { + return $formName . $prefix . "[$attribute]" . $suffix; + } else { + throw new InvalidParamException(get_class($model) . '::formName() cannot be empty for tabular inputs.'); + } + } + + public static function getInputId($model, $attribute) + { + $name = static::getInputName($model, $attribute); + return str_replace(array('[]', '][', '[', ']', ' '), array('', '-', '-', '', '-'), $name); + } } From 86f947e9eff42214293dcfab769bb837097d2fbe Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Thu, 4 Apr 2013 21:22:59 -0400 Subject: [PATCH 032/104] Finished DateValidator. --- framework/validators/DateValidator.php | 56 ++++++++++++++-------------------- 1 file changed, 23 insertions(+), 33 deletions(-) diff --git a/framework/validators/DateValidator.php b/framework/validators/DateValidator.php index 7899c95..7d0793d 100644 --- a/framework/validators/DateValidator.php +++ b/framework/validators/DateValidator.php @@ -7,11 +7,11 @@ namespace yii\validators; +use Yii; +use DateTime; + /** - * DateValidator verifies if the attribute represents a date, time or datetime. - * - * By setting the {@link format} property, one can specify what format the date value - * must be in. If the given date value doesn't follow the format, the attribute is considered as invalid. + * DateValidator verifies if the attribute represents a date, time or datetime in a proper format. * * @author Qiang Xue * @since 2.0 @@ -19,17 +19,11 @@ namespace yii\validators; class DateValidator extends Validator { /** - * @var mixed the format pattern that the date value should follow. - * This can be either a string or an array representing multiple formats. - * Defaults to 'MM/dd/yyyy'. Please see {@link CDateTimeParser} for details - * about how to specify a date format. - */ - public $format = 'MM/dd/yyyy'; - /** - * @var boolean whether the attribute value can be null or empty. Defaults to true, - * meaning that if the attribute is empty, it is considered valid. + * @var string the date format that the value being validated should follow. + * Please refer to [[http://www.php.net/manual/en/datetime.createfromformat.php]] on + * supported formats. */ - public $allowEmpty = true; + public $format = 'Y-m-d'; /** * @var string the name of the attribute to receive the parsing result. * When this property is not null and the validation is successful, the named attribute will @@ -46,27 +40,23 @@ class DateValidator extends Validator public function validateAttribute($object, $attribute) { $value = $object->$attribute; - if ($this->allowEmpty && $this->isEmpty($value)) { - return; - } - - $formats = is_string($this->format) ? array($this->format) : $this->format; - $valid = false; - foreach ($formats as $format) { - $timestamp = CDateTimeParser::parse($value, $format, array('month' => 1, 'day' => 1, 'hour' => 0, 'minute' => 0, 'second' => 0)); - if ($timestamp !== false) { - $valid = true; - if ($this->timestampAttribute !== null) { - $object-> {$this->timestampAttribute} = $timestamp; - } - break; - } - } - - if (!$valid) { - $message = ($this->message !== null) ? $this->message : \Yii::t('yii|The format of {attribute} is invalid.'); + $date = DateTime::createFromFormat($this->format, $value); + if ($date === false) { + $message = $this->message !== null ? $this->message : Yii::t('yii|The format of {attribute} is invalid.'); $this->addError($object, $attribute, $message); + } elseif ($this->timestampAttribute !== false) { + $object->{$this->timestampAttribute} = $date->getTimestamp(); } } + + /** + * Validates the given value. + * @param mixed $value the value to be validated. + * @return boolean whether the value is valid. + */ + public function validateValue($value) + { + return DateTime::createFromFormat($this->format, $value) !== false; + } } From ae6c69fc8fa2ca15299fe5de6e3c1a9045ab42ad Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Fri, 5 Apr 2013 08:32:13 -0400 Subject: [PATCH 033/104] refacotring validators. --- framework/validators/BooleanValidator.php | 19 +++++++--- framework/validators/CaptchaValidator.php | 19 +++++++--- framework/validators/CompareValidator.php | 2 +- framework/validators/DateValidator.php | 18 ++++++++-- framework/validators/EmailValidator.php | 21 ++++++++--- framework/validators/ExistValidator.php | 17 +++++++-- framework/validators/NumberValidator.php | 2 +- framework/validators/RangeValidator.php | 17 ++++----- .../validators/RegularExpressionValidator.php | 12 ++++--- framework/validators/RequiredValidator.php | 30 +++++++++------- framework/validators/StringValidator.php | 41 +++++++++++----------- framework/validators/UniqueValidator.php | 18 ++++++++-- framework/validators/UrlValidator.php | 22 +++++++++--- 13 files changed, 161 insertions(+), 77 deletions(-) diff --git a/framework/validators/BooleanValidator.php b/framework/validators/BooleanValidator.php index b441108..6d2c671 100644 --- a/framework/validators/BooleanValidator.php +++ b/framework/validators/BooleanValidator.php @@ -36,6 +36,17 @@ class BooleanValidator extends Validator public $strict = false; /** + * Initializes the validator. + */ + public function init() + { + parent::init(); + if ($this->message === null) { + $this->message = Yii::t('yii|{attribute} must be either "{true}" or "{false}".'); + } + } + + /** * Validates the attribute of the object. * If there is any error, the error message is added to the object. * @param \yii\base\Model $object the object being validated @@ -45,8 +56,7 @@ class BooleanValidator extends Validator { $value = $object->$attribute; if (!$this->validateValue($value)) { - $message = $this->message !== null ? $this->message : Yii::t('yii|{attribute} must be either "{true}" or "{false}".'); - $this->addError($object, $attribute, $message, array( + $this->addError($object, $attribute, $this->message, array( '{true}' => $this->trueValue, '{false}' => $this->falseValue, )); @@ -72,15 +82,14 @@ class BooleanValidator extends Validator */ public function clientValidateAttribute($object, $attribute) { - $message = ($this->message !== null) ? $this->message : Yii::t('yii|{attribute} must be either "{true}" or "{false}".'); - $message = strtr($message, array( + $message = strtr($this->message, array( '{attribute}' => $object->getAttributeLabel($attribute), '{value}' => $object->$attribute, '{true}' => $this->trueValue, '{false}' => $this->falseValue, )); return " -if(" . ($this->allowEmpty ? "$.trim(value)!='' && " : '') . "value!=" . json_encode($this->trueValue) . " && value!=" . json_encode($this->falseValue) . ") { +if(" . ($this->skipOnEmpty ? "$.trim(value)!='' && " : '') . "value!=" . json_encode($this->trueValue) . " && value!=" . json_encode($this->falseValue) . ") { messages.push(" . json_encode($message) . "); } "; diff --git a/framework/validators/CaptchaValidator.php b/framework/validators/CaptchaValidator.php index 65e7fd3..3b4745b 100644 --- a/framework/validators/CaptchaValidator.php +++ b/framework/validators/CaptchaValidator.php @@ -31,6 +31,17 @@ class CaptchaValidator extends Validator /** + * Initializes the validator. + */ + public function init() + { + parent::init(); + if ($this->message === null) { + $this->message = Yii::t('yii|The verification code is incorrect.'); + } + } + + /** * Validates the attribute of the object. * If there is any error, the error message is added to the object. * @param \yii\base\Model $object the object being validated @@ -40,8 +51,7 @@ class CaptchaValidator extends Validator { $value = $object->$attribute; if (!$this->validateValue($value)) { - $message = $this->message !== null ? $this->message : Yii::t('yii|The verification code is incorrect.'); - $this->addError($object, $attribute, $message); + $this->addError($object, $attribute, $this->message); } } @@ -83,8 +93,7 @@ class CaptchaValidator extends Validator public function clientValidateAttribute($object, $attribute) { $captcha = $this->getCaptchaAction(); - $message = $this->message !== null ? $this->message : \Yii::t('yii|The verification code is incorrect.'); - $message = strtr($message, array( + $message = strtr($this->message, array( '{attribute}' => $object->getAttributeLabel($attribute), '{value}' => $object->$attribute, )); @@ -102,7 +111,7 @@ if(h != hash) { } "; - if ($this->allowEmpty) { + if ($this->skipOnEmpty) { $js = " if($.trim(value)!='') { $js diff --git a/framework/validators/CompareValidator.php b/framework/validators/CompareValidator.php index 3c85367..eb3edc6 100644 --- a/framework/validators/CompareValidator.php +++ b/framework/validators/CompareValidator.php @@ -223,7 +223,7 @@ class CompareValidator extends Validator )); return " -if (" . ($this->allowEmpty ? "$.trim(value)!='' && " : '') . $condition . ") { +if (" . ($this->skipOnEmpty ? "$.trim(value)!='' && " : '') . $condition . ") { messages.push(" . json_encode($message) . "); } "; diff --git a/framework/validators/DateValidator.php b/framework/validators/DateValidator.php index 7d0793d..7c3b181 100644 --- a/framework/validators/DateValidator.php +++ b/framework/validators/DateValidator.php @@ -32,6 +32,17 @@ class DateValidator extends Validator public $timestampAttribute; /** + * Initializes the validator. + */ + public function init() + { + parent::init(); + if ($this->message === null) { + $this->message = Yii::t('yii|The format of {attribute} is invalid.'); + } + } + + /** * Validates the attribute of the object. * If there is any error, the error message is added to the object. * @param \yii\base\Model $object the object being validated @@ -40,10 +51,13 @@ class DateValidator extends Validator public function validateAttribute($object, $attribute) { $value = $object->$attribute; + if (is_array($value)) { + $this->addError($object, $attribute, $this->message); + return; + } $date = DateTime::createFromFormat($this->format, $value); if ($date === false) { - $message = $this->message !== null ? $this->message : Yii::t('yii|The format of {attribute} is invalid.'); - $this->addError($object, $attribute, $message); + $this->addError($object, $attribute, $this->message); } elseif ($this->timestampAttribute !== false) { $object->{$this->timestampAttribute} = $date->getTimestamp(); } diff --git a/framework/validators/EmailValidator.php b/framework/validators/EmailValidator.php index 396c25f..e498975 100644 --- a/framework/validators/EmailValidator.php +++ b/framework/validators/EmailValidator.php @@ -7,6 +7,8 @@ namespace yii\validators; +use Yii; + /** * EmailValidator validates that the attribute value is a valid email address. * @@ -44,6 +46,17 @@ class EmailValidator extends Validator public $checkPort = false; /** + * Initializes the validator. + */ + public function init() + { + parent::init(); + if ($this->message === null) { + $this->message = Yii::t('yii|{attribute} is not a valid email address.'); + } + } + + /** * Validates the attribute of the object. * If there is any error, the error message is added to the object. * @param \yii\base\Model $object the object being validated @@ -53,8 +66,7 @@ class EmailValidator extends Validator { $value = $object->$attribute; if (!$this->validateValue($value)) { - $message = ($this->message !== null) ? $this->message : \Yii::t('yii|{attribute} is not a valid email address.'); - $this->addError($object, $attribute, $message); + $this->addError($object, $attribute, $this->message); } } @@ -88,8 +100,7 @@ class EmailValidator extends Validator */ public function clientValidateAttribute($object, $attribute) { - $message = ($this->message !== null) ? $this->message : \Yii::t('yii|{attribute} is not a valid email address.'); - $message = strtr($message, array( + $message = strtr($this->message, array( '{attribute}' => $object->getAttributeLabel($attribute), '{value}' => $object->$attribute, )); @@ -100,7 +111,7 @@ class EmailValidator extends Validator } return " -if(" . ($this->allowEmpty ? "$.trim(value)!='' && " : '') . $condition . ") { +if(" . ($this->skipOnEmpty ? "$.trim(value)!='' && " : '') . $condition . ") { messages.push(" . json_encode($message) . "); } "; diff --git a/framework/validators/ExistValidator.php b/framework/validators/ExistValidator.php index ec01134..7aa434c 100644 --- a/framework/validators/ExistValidator.php +++ b/framework/validators/ExistValidator.php @@ -37,6 +37,18 @@ class ExistValidator extends Validator */ public $attributeName; + + /** + * Initializes the validator. + */ + public function init() + { + parent::init(); + if ($this->message === null) { + $this->message = Yii::t('yii|{attribute} is invalid.'); + } + } + /** * Validates the attribute of the object. * If there is any error, the error message is added to the object. @@ -49,7 +61,7 @@ class ExistValidator extends Validator $value = $object->$attribute; if (is_array($value)) { - $this->addError($object, $attribute, Yii::t('yii|{attribute} is invalid.')); + $this->addError($object, $attribute, $this->message); return; } @@ -59,8 +71,7 @@ class ExistValidator extends Validator $query = $className::find(); $query->where(array($attributeName => $value)); if (!$query->exists()) { - $message = $this->message !== null ? $this->message : Yii::t('yii|{attribute} "{value}" is invalid.'); - $this->addError($object, $attribute, $message); + $this->addError($object, $attribute, $this->message); } } diff --git a/framework/validators/NumberValidator.php b/framework/validators/NumberValidator.php index 4d7297f..226df12 100644 --- a/framework/validators/NumberValidator.php +++ b/framework/validators/NumberValidator.php @@ -152,7 +152,7 @@ if(value>{$this->max}) { "; } - if ($this->allowEmpty) { + if ($this->skipOnEmpty) { $js = " if(jQuery.trim(value)!='') { $js diff --git a/framework/validators/RangeValidator.php b/framework/validators/RangeValidator.php index 0498a55..18742ae 100644 --- a/framework/validators/RangeValidator.php +++ b/framework/validators/RangeValidator.php @@ -6,6 +6,8 @@ */ namespace yii\validators; + +use Yii; use yii\base\InvalidConfigException; /** @@ -44,6 +46,9 @@ class RangeValidator extends Validator if (!is_array($this->range)) { throw new InvalidConfigException('The "range" property must be set.'); } + if ($this->message === null) { + $this->message = Yii::t('yii|{attribute} is invalid.'); + } } /** @@ -55,11 +60,10 @@ class RangeValidator extends Validator public function validateAttribute($object, $attribute) { $value = $object->$attribute; - $message = $this->message !== null ? $this->message : \Yii::t('yii|{attribute} is invalid.'); if (!$this->not && !in_array($value, $this->range, $this->strict)) { - $this->addError($object, $attribute, $message); + $this->addError($object, $attribute, $this->message); } elseif ($this->not && in_array($value, $this->range, $this->strict)) { - $this->addError($object, $attribute, $message); + $this->addError($object, $attribute, $this->message); } } @@ -82,10 +86,7 @@ class RangeValidator extends Validator */ public function clientValidateAttribute($object, $attribute) { - if (($message = $this->message) === null) { - $message = \Yii::t('yii|{attribute} is invalid.'); - } - $message = strtr($message, array( + $message = strtr($this->message, array( '{attribute}' => $object->getAttributeLabel($attribute), '{value}' => $object->$attribute, )); @@ -97,7 +98,7 @@ class RangeValidator extends Validator $range = json_encode($range); return " -if (" . ($this->allowEmpty ? "$.trim(value)!='' && " : '') . ($this->not ? "$.inArray(value, $range)>=0" : "$.inArray(value, $range)<0") . ") { +if (" . ($this->skipOnEmpty ? "$.trim(value)!='' && " : '') . ($this->not ? "$.inArray(value, $range)>=0" : "$.inArray(value, $range)<0") . ") { messages.push(" . json_encode($message) . "); } "; diff --git a/framework/validators/RegularExpressionValidator.php b/framework/validators/RegularExpressionValidator.php index d88f613..6c69be3 100644 --- a/framework/validators/RegularExpressionValidator.php +++ b/framework/validators/RegularExpressionValidator.php @@ -7,6 +7,7 @@ namespace yii\validators; +use Yii; use yii\base\InvalidConfigException; /** @@ -40,6 +41,9 @@ class RegularExpressionValidator extends Validator if ($this->pattern === null) { throw new InvalidConfigException('The "pattern" property must be set.'); } + if ($this->message === null) { + $this->message = Yii::t('yii|{attribute} is invalid.'); + } } /** @@ -52,8 +56,7 @@ class RegularExpressionValidator extends Validator { $value = $object->$attribute; if (!$this->validateValue($value)) { - $message = $this->message !== null ? $this->message : \Yii::t('yii|{attribute} is invalid.'); - $this->addError($object, $attribute, $message); + $this->addError($object, $attribute, $this->message); } } @@ -78,8 +81,7 @@ class RegularExpressionValidator extends Validator */ public function clientValidateAttribute($object, $attribute) { - $message = ($this->message !== null) ? $this->message : \Yii::t('yii|{attribute} is invalid.'); - $message = strtr($message, array( + $message = strtr($this->message, array( '{attribute}' => $object->getAttributeLabel($attribute), '{value}' => $object->$attribute, )); @@ -99,7 +101,7 @@ class RegularExpressionValidator extends Validator } return " -if (" . ($this->allowEmpty ? "$.trim(value)!='' && " : '') . ($this->not ? '' : '!') . "value.match($pattern)) { +if (" . ($this->skipOnEmpty ? "$.trim(value)!='' && " : '') . ($this->not ? '' : '!') . "value.match($pattern)) { messages.push(" . json_encode($message) . "); } "; diff --git a/framework/validators/RequiredValidator.php b/framework/validators/RequiredValidator.php index 944c695..febee9b 100644 --- a/framework/validators/RequiredValidator.php +++ b/framework/validators/RequiredValidator.php @@ -7,6 +7,8 @@ namespace yii\validators; +use Yii; + /** * RequiredValidator validates that the specified attribute does not have null or empty value. * @@ -39,6 +41,17 @@ class RequiredValidator extends Validator public $strict = false; /** + * Initializes the validator. + */ + public function init() + { + parent::init(); + if ($this->message === null) { + $this->message = $this->requiredValue === null ? Yii::t('yii|{attribute} is invalid.') : Yii::t('yii|{attribute} must be "{requiredValue}".'); + } + } + + /** * Validates the attribute of the object. * If there is any error, the error message is added to the object. * @param \yii\base\Model $object the object being validated @@ -49,13 +62,11 @@ class RequiredValidator extends Validator $value = $object->$attribute; if ($this->requiredValue === null) { if ($this->strict && $value === null || !$this->strict && $this->isEmpty($value, true)) { - $message = ($this->message !== null) ? $this->message : \Yii::t('yii|{attribute} cannot be blank.'); - $this->addError($object, $attribute, $message); + $this->addError($object, $attribute, $this->message); } } else { if (!$this->strict && $value != $this->requiredValue || $this->strict && $value !== $this->requiredValue) { - $message = ($this->message !== null) ? $this->message : \Yii::t('yii|{attribute} must be "{requiredValue}".'); - $this->addError($object, $attribute, $message, array( + $this->addError($object, $attribute, $this->message, array( '{requiredValue}' => $this->requiredValue, )); } @@ -87,12 +98,8 @@ class RequiredValidator extends Validator */ public function clientValidateAttribute($object, $attribute) { - $message = $this->message; if ($this->requiredValue !== null) { - if ($message === null) { - $message = \Yii::t('yii|{attribute} must be "{requiredValue}".'); - } - $message = strtr($message, array( + $message = strtr($this->message, array( '{attribute}' => $object->getAttributeLabel($attribute), '{value}' => $object->$attribute, '{requiredValue}' => $this->requiredValue, @@ -103,10 +110,7 @@ if (value != " . json_encode($this->requiredValue) . ") { } "; } else { - if ($message === null) { - $message = \Yii::t('yii|{attribute} cannot be blank.'); - } - $message = strtr($message, array( + $message = strtr($this->message, array( '{attribute}' => $object->getAttributeLabel($attribute), '{value}' => $object->$attribute, )); diff --git a/framework/validators/StringValidator.php b/framework/validators/StringValidator.php index 83ff35b..8b8c73b 100644 --- a/framework/validators/StringValidator.php +++ b/framework/validators/StringValidator.php @@ -63,6 +63,18 @@ class StringValidator extends Validator if ($this->encoding === null) { $this->encoding = Yii::$app->charset; } + if ($this->message === null) { + $this->message = Yii::t('yii|{attribute} must be a string.'); + } + if ($this->min !== null && $this->tooShort === null) { + $this->tooShort = Yii::t('yii|{attribute} should contain at least {min} characters.'); + } + if ($this->max !== null && $this->tooLong === null) { + $this->tooLong = Yii::t('yii|{attribute} should contain at most {max} characters.'); + } + if ($this->is !== null && $this->notEqual === null) { + $this->notEqual = Yii::t('yii|{attribute} should contain {length} characters.'); + } } /** @@ -76,24 +88,20 @@ class StringValidator extends Validator $value = $object->$attribute; if (!is_string($value)) { - $message = ($this->message !== null) ? $this->message : Yii::t('yii|{attribute} must be a string.'); - $this->addError($object, $attribute, $message); + $this->addError($object, $attribute, $this->message); return; } $length = mb_strlen($value, $this->encoding); if ($this->min !== null && $length < $this->min) { - $message = ($this->tooShort !== null) ? $this->tooShort : Yii::t('yii|{attribute} should contain at least {min} characters.'); - $this->addError($object, $attribute, $message, array('{min}' => $this->min)); + $this->addError($object, $attribute, $this->tooShort, array('{min}' => $this->min)); } if ($this->max !== null && $length > $this->max) { - $message = ($this->tooLong !== null) ? $this->tooLong : Yii::t('yii|{attribute} should contain at most {max} characters.'); - $this->addError($object, $attribute, $message, array('{max}' => $this->max)); + $this->addError($object, $attribute, $this->tooLong, array('{max}' => $this->max)); } if ($this->is !== null && $length !== $this->is) { - $message = ($this->notEqual !== null) ? $this->notEqual : Yii::t('yii|{attribute} should contain {length} characters.'); - $this->addError($object, $attribute, $message, array('{length}' => $this->is)); + $this->addError($object, $attribute, $this->notEqual, array('{length}' => $this->is)); } } @@ -124,28 +132,19 @@ class StringValidator extends Validator $label = $object->getAttributeLabel($attribute); $value = $object->$attribute; - if (($notEqual = $this->notEqual) === null) { - $notEqual = Yii::t('yii|{attribute} should contain {length} characters.'); - } - $notEqual = strtr($notEqual, array( + $notEqual = strtr($this->notEqual, array( '{attribute}' => $label, '{value}' => $value, '{length}' => $this->is, )); - if (($tooShort = $this->tooShort) === null) { - $tooShort = Yii::t('yii|{attribute} should contain at least {min} characters.'); - } - $tooShort = strtr($tooShort, array( + $tooShort = strtr($this->tooShort, array( '{attribute}' => $label, '{value}' => $value, '{min}' => $this->min, )); - if (($tooLong = $this->tooLong) === null) { - $tooLong = Yii::t('yii|{attribute} should contain at most {max} characters.'); - } - $tooLong = strtr($tooLong, array( + $tooLong = strtr($this->tooLong, array( '{attribute}' => $label, '{value}' => $value, '{max}' => $this->max, @@ -174,7 +173,7 @@ if(value.length!= {$this->is}) { "; } - if ($this->allowEmpty) { + if ($this->skipOnEmpty) { $js = " if($.trim(value)!='') { $js diff --git a/framework/validators/UniqueValidator.php b/framework/validators/UniqueValidator.php index 30735b1..fa55df7 100644 --- a/framework/validators/UniqueValidator.php +++ b/framework/validators/UniqueValidator.php @@ -6,6 +6,8 @@ */ namespace yii\validators; + +use Yii; use yii\base\InvalidConfigException; /** @@ -31,6 +33,17 @@ class UniqueValidator extends Validator public $attributeName; /** + * Initializes the validator. + */ + public function init() + { + parent::init(); + if ($this->message === null) { + $this->message = Yii::t('yii|{attribute} "{value}" has already been taken.'); + } + } + + /** * Validates the attribute of the object. * If there is any error, the error message is added to the object. * @param \yii\db\ActiveRecord $object the object being validated @@ -52,7 +65,7 @@ class UniqueValidator extends Validator $table = $className::getTableSchema(); if (($column = $table->getColumn($attributeName)) === null) { - throw new InvalidConfigException('Table "' . $table->name . '" does not have a column named "' . $attributeName . '"'); + throw new InvalidConfigException("Table '{$table->name}' does not have a column named '$attributeName'."); } $query = $className::find(); @@ -81,8 +94,7 @@ class UniqueValidator extends Validator } if ($exists) { - $message = $this->message !== null ? $this->message : \Yii::t('yii|{attribute} "{value}" has already been taken.'); - $this->addError($object, $attribute, $message); + $this->addError($object, $attribute, $this->message); } } } \ No newline at end of file diff --git a/framework/validators/UrlValidator.php b/framework/validators/UrlValidator.php index fb743e0..cd6bfef 100644 --- a/framework/validators/UrlValidator.php +++ b/framework/validators/UrlValidator.php @@ -7,6 +7,8 @@ namespace yii\validators; +use Yii; + /** * UrlValidator validates that the attribute value is a valid http or https URL. * @@ -33,6 +35,18 @@ class UrlValidator extends Validator **/ public $defaultScheme; + + /** + * Initializes the validator. + */ + public function init() + { + parent::init(); + if ($this->message === null) { + $this->message = Yii::t('yii|{attribute} is not a valid URL.'); + } + } + /** * Validates the attribute of the object. * If there is any error, the error message is added to the object. @@ -47,8 +61,7 @@ class UrlValidator extends Validator $object->$attribute = $this->defaultScheme . '://' . $value; } } else { - $message = ($this->message !== null) ? $this->message : \Yii::t('yii|{attribute} is not a valid URL.'); - $this->addError($object, $attribute, $message); + $this->addError($object, $attribute, $this->message); } } @@ -87,8 +100,7 @@ class UrlValidator extends Validator */ public function clientValidateAttribute($object, $attribute) { - $message = ($this->message !== null) ? $this->message : \Yii::t('yii|{attribute} is not a valid URL.'); - $message = strtr($message, array( + $message = strtr($this->message, array( '{attribute}' => $object->getAttributeLabel($attribute), '{value}' => $object->$attribute, )); @@ -113,7 +125,7 @@ $js "; } - if ($this->allowEmpty) { + if ($this->skipOnEmpty) { $js = " if($.trim(value)!='') { $js From 52a160cb7beaf9fe39ff36cd246b84b80e509e17 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Fri, 5 Apr 2013 10:21:43 -0400 Subject: [PATCH 034/104] refactored validators. --- framework/validators/CompareValidator.php | 158 +++++++++++------------------ framework/validators/NumberValidator.php | 54 +++++----- framework/validators/RequiredValidator.php | 3 +- framework/validators/Validator.php | 3 +- 4 files changed, 91 insertions(+), 127 deletions(-) diff --git a/framework/validators/CompareValidator.php b/framework/validators/CompareValidator.php index eb3edc6..1df09c4 100644 --- a/framework/validators/CompareValidator.php +++ b/framework/validators/CompareValidator.php @@ -59,6 +59,45 @@ class CompareValidator extends Validator */ public $operator = '='; + + /** + * Initializes the validator. + */ + public function init() + { + parent::init(); + if ($this->message === null) { + switch ($this->operator) { + case '==': + $this->message = Yii::t('yii|{attribute} must be repeated exactly.'); + break; + case '===': + $this->message = Yii::t('yii|{attribute} must be repeated exactly.'); + break; + case '!=': + $this->message = Yii::t('yii|{attribute} must not be equal to "{compareValue}".'); + break; + case '!==': + $this->message = Yii::t('yii|{attribute} must not be equal to "{compareValue}".'); + break; + case '>': + $this->message = Yii::t('yii|{attribute} must be greater than "{compareValue}".'); + break; + case '>=': + $this->message = Yii::t('yii|{attribute} must be greater than or equal to "{compareValue}".'); + break; + case '<': + $this->message = Yii::t('yii|{attribute} must be less than "{compareValue}".'); + break; + case '<=': + $this->message = Yii::t('yii|{attribute} must be less than or equal to "{compareValue}".'); + break; + default: + throw new InvalidConfigException("Unknown operator: {$this->operator}"); + } + } + } + /** * Validates the attribute of the object. * If there is any error, the error message is added to the object. @@ -69,6 +108,10 @@ class CompareValidator extends Validator public function validateAttribute($object, $attribute) { $value = $object->$attribute; + if (is_array($value)) { + $this->addError($object, $attribute, Yii::t('yii|{attribute} is invalid.')); + return; + } if ($this->compareValue !== null) { $compareLabel = $compareValue = $this->compareValue; } else { @@ -78,56 +121,21 @@ class CompareValidator extends Validator } switch ($this->operator) { - case '==': - if ($value != $compareValue) { - $message = ($this->message !== null) ? $this->message : Yii::t('yii|{attribute} must be repeated exactly.'); - $this->addError($object, $attribute, $message, array('{compareAttribute}' => $compareLabel)); - } - break; - case '===': - if ($value !== $compareValue) { - $message = ($this->message !== null) ? $this->message : Yii::t('yii|{attribute} must be repeated exactly.'); - $this->addError($object, $attribute, $message, array('{compareAttribute}' => $compareLabel)); - } - break; - case '!=': - if ($value == $compareValue) { - $message = ($this->message !== null) ? $this->message : Yii::t('yii|{attribute} must not be equal to "{compareValue}".'); - $this->addError($object, $attribute, $message, array('{compareAttribute}' => $compareLabel, '{compareValue}' => $compareValue)); - } - break; - case '!==': - if ($value === $compareValue) { - $message = ($this->message !== null) ? $this->message : Yii::t('yii|{attribute} must not be equal to "{compareValue}".'); - $this->addError($object, $attribute, $message, array('{compareAttribute}' => $compareLabel, '{compareValue}' => $compareValue)); - } - break; - case '>': - if ($value <= $compareValue) { - $message = ($this->message !== null) ? $this->message : Yii::t('yii|{attribute} must be greater than "{compareValue}".'); - $this->addError($object, $attribute, $message, array('{compareAttribute}' => $compareLabel, '{compareValue}' => $compareValue)); - } - break; - case '>=': - if ($value < $compareValue) { - $message = ($this->message !== null) ? $this->message : Yii::t('yii|{attribute} must be greater than or equal to "{compareValue}".'); - $this->addError($object, $attribute, $message, array('{compareAttribute}' => $compareLabel, '{compareValue}' => $compareValue)); - } - break; - case '<': - if ($value >= $compareValue) { - $message = ($this->message !== null) ? $this->message : Yii::t('yii|{attribute} must be less than "{compareValue}".'); - $this->addError($object, $attribute, $message, array('{compareAttribute}' => $compareLabel, '{compareValue}' => $compareValue)); - } - break; - case '<=': - if ($value > $compareValue) { - $message = ($this->message !== null) ? $this->message : Yii::t('yii|{attribute} must be less than or equal to "{compareValue}".'); - $this->addError($object, $attribute, $message, array('{compareAttribute}' => $compareLabel, '{compareValue}' => $compareValue)); - } - break; - default: - throw new InvalidConfigException("Unknown operator: {$this->operator}"); + case '==': $valid = $value == $compareValue; break; + case '===': $valid = $value === $compareValue; break; + case '!=': $valid = $value != $compareValue; break; + case '!==': $valid = $value !== $compareValue; break; + case '>': $valid = $value > $compareValue; break; + case '>=': $valid = $value >= $compareValue; break; + case '<': $valid = $value < $compareValue; break; + case '<=': $valid = $value <= $compareValue; break; + default: $valid = false; break; + } + if (!$valid) { + $this->addError($object, $attribute, $this->message, array( + '{compareAttribute}' => $compareLabel, + '{compareValue}' => $compareValue, + )); } } @@ -135,6 +143,7 @@ class CompareValidator extends Validator * Validates the given value. * @param mixed $value the value to be validated. * @return boolean whether the value is valid. + * @throws InvalidConfigException if [[compareValue]] is not set. */ public function validateValue($value) { @@ -151,8 +160,6 @@ class CompareValidator extends Validator case '>=': return $value >= $this->compareValue; case '<': return $value < $this->compareValue; case '<=': return $value <= $this->compareValue; - default: - throw new InvalidConfigException("Unknown operator \"{$this->operator}\""); } } @@ -173,51 +180,8 @@ class CompareValidator extends Validator $compareValue = "\$('#" . (CHtml::activeId($object, $compareAttribute)) . "').val()"; $compareLabel = $object->getAttributeLabel($compareAttribute); } - - $message = $this->message; - switch ($this->operator) { - case '=': - case '==': - if ($message === null) { - $message = Yii::t('yii|{attribute} must be repeated exactly.'); - } - $condition = 'value!=' . $compareValue; - break; - case '!=': - if ($message === null) { - $message = Yii::t('yii|{attribute} must not be equal to "{compareValue}".'); - } - $condition = 'value==' . $compareValue; - break; - case '>': - if ($message === null) { - $message = Yii::t('yii|{attribute} must be greater than "{compareValue}".'); - } - $condition = 'value<=' . $compareValue; - break; - case '>=': - if ($message === null) { - $message = Yii::t('yii|{attribute} must be greater than or equal to "{compareValue}".'); - } - $condition = 'value<' . $compareValue; - break; - case '<': - if ($message === null) { - $message = Yii::t('yii|{attribute} must be less than "{compareValue}".'); - } - $condition = 'value>=' . $compareValue; - break; - case '<=': - if ($message === null) { - $message = Yii::t('yii|{attribute} must be less than or equal to "{compareValue}".'); - } - $condition = 'value>' . $compareValue; - break; - default: - throw new InvalidConfigException("Unknown operator: {$this->operator}"); - } - - $message = strtr($message, array( + $condition = "value {$this->operator} $compareValue"; + $message = strtr($this->message, array( '{attribute}' => $object->getAttributeLabel($attribute), '{compareValue}' => $compareLabel, )); diff --git a/framework/validators/NumberValidator.php b/framework/validators/NumberValidator.php index 226df12..915419e 100644 --- a/framework/validators/NumberValidator.php +++ b/framework/validators/NumberValidator.php @@ -53,6 +53,24 @@ class NumberValidator extends Validator /** + * Initializes the validator. + */ + public function init() + { + parent::init(); + if ($this->message === null) { + $this->message = $this->integerOnly ? Yii::t('yii|{attribute} must be an integer.') + : Yii::t('yii|{attribute} must be a number.'); + } + if ($this->min !== null && $this->tooSmall === null) { + $this->tooSmall = Yii::t('yii|{attribute} must be no less than {min}.'); + } + if ($this->max !== null && $this->tooBig === null) { + $this->tooBig = Yii::t('yii|{attribute} must be no greater than {max}.'); + } + } + + /** * Validates the attribute of the object. * If there is any error, the error message is added to the object. * @param \yii\base\Model $object the object being validated @@ -65,24 +83,15 @@ class NumberValidator extends Validator $this->addError($object, $attribute, Yii::t('yii|{attribute} is invalid.')); return; } - if ($this->integerOnly) { - if (!preg_match($this->integerPattern, "$value")) { - $message = $this->message !== null ? $this->message : Yii::t('yii|{attribute} must be an integer.'); - $this->addError($object, $attribute, $message); - } - } else { - if (!preg_match($this->numberPattern, "$value")) { - $message = $this->message !== null ? $this->message : Yii::t('yii|{attribute} must be a number.'); - $this->addError($object, $attribute, $message); - } + $pattern = $this->integerOnly ? $this->integerPattern : $this->numberPattern; + if (!preg_match($pattern, "$value")) { + $this->addError($object, $attribute, $this->message); } if ($this->min !== null && $value < $this->min) { - $message = $this->tooSmall !== null ? $this->tooSmall : Yii::t('yii|{attribute} must be no less than {min}.'); - $this->addError($object, $attribute, $message, array('{min}' => $this->min)); + $this->addError($object, $attribute, $this->tooSmall, array('{min}' => $this->min)); } if ($this->max !== null && $value > $this->max) { - $message = $this->tooBig !== null ? $this->tooBig : Yii::t('yii|{attribute} must be no greater than {max}.'); - $this->addError($object, $attribute, $message, array('{max}' => $this->max)); + $this->addError($object, $attribute, $this->tooBig, array('{max}' => $this->max)); } } @@ -107,12 +116,7 @@ class NumberValidator extends Validator public function clientValidateAttribute($object, $attribute) { $label = $object->getAttributeLabel($attribute); - - if (($message = $this->message) === null) { - $message = $this->integerOnly ? Yii::t('yii|{attribute} must be an integer.') - : Yii::t('yii|{attribute} must be a number.'); - } - $message = strtr($message, array( + $message = strtr($this->message, array( '{attribute}' => $label, )); @@ -123,10 +127,7 @@ if(!value.match($pattern)) { } "; if ($this->min !== null) { - if (($tooSmall = $this->tooSmall) === null) { - $tooSmall = Yii::t('yii|{attribute} must be no less than {min}.'); - } - $tooSmall = strtr($tooSmall, array( + $tooSmall = strtr($this->tooSmall, array( '{attribute}' => $label, '{min}' => $this->min, )); @@ -138,10 +139,7 @@ if(value<{$this->min}) { "; } if ($this->max !== null) { - if (($tooBig = $this->tooBig) === null) { - $tooBig = Yii::t('yii|{attribute} must be no greater than {max}.'); - } - $tooBig = strtr($tooBig, array( + $tooBig = strtr($this->tooBig, array( '{attribute}' => $label, '{max}' => $this->max, )); diff --git a/framework/validators/RequiredValidator.php b/framework/validators/RequiredValidator.php index febee9b..3b13eb3 100644 --- a/framework/validators/RequiredValidator.php +++ b/framework/validators/RequiredValidator.php @@ -47,7 +47,8 @@ class RequiredValidator extends Validator { parent::init(); if ($this->message === null) { - $this->message = $this->requiredValue === null ? Yii::t('yii|{attribute} is invalid.') : Yii::t('yii|{attribute} must be "{requiredValue}".'); + $this->message = $this->requiredValue === null ? Yii::t('yii|{attribute} is invalid.') + : Yii::t('yii|{attribute} must be "{requiredValue}".'); } } diff --git a/framework/validators/Validator.php b/framework/validators/Validator.php index b75f86e..5ab8dfe 100644 --- a/framework/validators/Validator.php +++ b/framework/validators/Validator.php @@ -245,8 +245,9 @@ abstract class Validator extends Component */ public function addError($object, $attribute, $message, $params = array()) { + $value = $object->$attribute; $params['{attribute}'] = $object->getAttributeLabel($attribute); - $params['{value}'] = $object->$attribute; + $params['{value}'] = is_array($value) ? 'array()' : $value; $object->addError($attribute, strtr($message, $params)); } From 31777c9231cdf69332405078d458c490725018b4 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Fri, 5 Apr 2013 12:43:55 -0400 Subject: [PATCH 035/104] Fixed render issue. --- framework/base/Controller.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/framework/base/Controller.php b/framework/base/Controller.php index 241c66c..6ff68da 100644 --- a/framework/base/Controller.php +++ b/framework/base/Controller.php @@ -357,7 +357,8 @@ class Controller extends Component */ public function renderPartial($view, $params = array()) { - return $this->getView()->render($view, $params, $this); + $viewFile = $this->findViewFile($view); + return $this->getView()->renderFile($viewFile, $params, $this); } /** From a421f9f1ab795cf4c21e4f9490d56d8957760a9e Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Fri, 5 Apr 2013 19:29:10 -0400 Subject: [PATCH 036/104] refactored component event. --- docs/api/base/Component.md | 24 +++-- framework/base/Component.php | 141 ++++++++++++++-------------- framework/base/Event.php | 9 +- tests/unit/framework/base/ComponentTest.php | 44 +++++---- 4 files changed, 107 insertions(+), 111 deletions(-) diff --git a/docs/api/base/Component.md b/docs/api/base/Component.md index 89b8c0b..b5a07fd 100644 --- a/docs/api/base/Component.md +++ b/docs/api/base/Component.md @@ -7,20 +7,20 @@ is triggered (i.e. comment will be added), our custom code will be executed. An event is identified by a name that should be unique within the class it is defined at. Event names are *case-sensitive*. -One or multiple PHP callbacks, called *event handlers*, could be attached to an event. You can call [[trigger()]] to +One or multiple PHP callbacks, called *event handlers*, can be attached to an event. You can call [[trigger()]] to raise an event. When an event is raised, the event handlers will be invoked automatically in the order they were attached. To attach an event handler to an event, call [[on()]]: ~~~ -$comment->on('add', function($event) { +$post->on('update', function($event) { // send email notification }); ~~~ -In the above, we attach an anonymous function to the "add" event of the comment. -Valid event handlers include: +In the above, an anonymous function is attached to the "update" event of the post. You may attach +the following types of event handlers: - anonymous function: `function($event) { ... }` - object method: `array($object, 'handleAdd')` @@ -35,8 +35,8 @@ function foo($event) where `$event` is an [[Event]] object which includes parameters associated with the event. -You can also attach an event handler to an event when configuring a component with a configuration array. The syntax is -like the following: +You can also attach a handler to an event when configuring a component with a configuration array. +The syntax is like the following: ~~~ array( @@ -46,15 +46,13 @@ array( where `on add` stands for attaching an event to the `add` event. -You can call [[getEventHandlers()]] to retrieve all event handlers that are attached to a specified event. Because this -method returns a [[Vector]] object, we can manipulate this object to attach/detach event handlers, or adjust their -relative orders. +Sometimes, you may want to associate extra data with an event handler when you attach it to an event +and then access it when the handler is invoked. You may do so by ~~~ -$handlers = $comment->getEventHandlers('add'); -$handlers->insertAt(0, $callback); // attach a handler as the first one -$handlers[] = $callback; // attach a handler as the last one -unset($handlers[0]); // detach the first handler +$post->on('update', function($event) { + // the data can be accessed via $event->data +}, $data); ~~~ diff --git a/framework/base/Component.php b/framework/base/Component.php index f1d549b..5a76ee3 100644 --- a/framework/base/Component.php +++ b/framework/base/Component.php @@ -7,6 +7,8 @@ namespace yii\base; +use Yii; + /** * Component is the base class that provides the *property*, *event* and *behavior* features. * @@ -17,16 +19,16 @@ namespace yii\base; * @author Qiang Xue * @since 2.0 */ -class Component extends \yii\base\Object +class Component extends Object { /** - * @var Vector[] the attached event handlers (event name => handlers) + * @var array the attached event handlers (event name => handlers) */ - private $_e; + private $_events; /** * @var Behavior[] the attached behaviors (behavior name => behavior) */ - private $_b; + private $_behaviors; /** * Returns the value of a component property. @@ -52,7 +54,7 @@ class Component extends \yii\base\Object } else { // behavior property $this->ensureBehaviors(); - foreach ($this->_b as $behavior) { + foreach ($this->_behaviors as $behavior) { if ($behavior->canGetProperty($name)) { return $behavior->$name; } @@ -87,17 +89,16 @@ class Component extends \yii\base\Object return; } elseif (strncmp($name, 'on ', 3) === 0) { // on event: attach event handler - $name = trim(substr($name, 3)); - $this->getEventHandlers($name)->add($value); + $this->on(trim(substr($name, 3)), $value); return; } elseif (strncmp($name, 'as ', 3) === 0) { // as behavior: attach behavior $name = trim(substr($name, 3)); - $this->attachBehavior($name, $value instanceof Behavior ? $value : \Yii::createObject($value)); + $this->attachBehavior($name, $value instanceof Behavior ? $value : Yii::createObject($value)); } else { // behavior property $this->ensureBehaviors(); - foreach ($this->_b as $behavior) { + foreach ($this->_behaviors as $behavior) { if ($behavior->canSetProperty($name)) { $behavior->$name = $value; return; @@ -131,7 +132,7 @@ class Component extends \yii\base\Object } else { // behavior property $this->ensureBehaviors(); - foreach ($this->_b as $behavior) { + foreach ($this->_behaviors as $behavior) { if ($behavior->canGetProperty($name)) { return $behavior->$name !== null; } @@ -161,7 +162,7 @@ class Component extends \yii\base\Object } else { // behavior property $this->ensureBehaviors(); - foreach ($this->_b as $behavior) { + foreach ($this->_behaviors as $behavior) { if ($behavior->canSetProperty($name)) { $behavior->$name = null; return; @@ -198,7 +199,7 @@ class Component extends \yii\base\Object } $this->ensureBehaviors(); - foreach ($this->_b as $object) { + foreach ($this->_behaviors as $object) { if (method_exists($object, $name)) { return call_user_func_array(array($object, $name), $params); } @@ -213,8 +214,8 @@ class Component extends \yii\base\Object */ public function __clone() { - $this->_e = null; - $this->_b = null; + $this->_events = null; + $this->_behaviors = null; } /** @@ -259,7 +260,7 @@ class Component extends \yii\base\Object return true; } else { $this->ensureBehaviors(); - foreach ($this->_b as $behavior) { + foreach ($this->_behaviors as $behavior) { if ($behavior->canGetProperty($name, $checkVar)) { return true; } @@ -289,7 +290,7 @@ class Component extends \yii\base\Object return true; } else { $this->ensureBehaviors(); - foreach ($this->_b as $behavior) { + foreach ($this->_behaviors as $behavior) { if ($behavior->canSetProperty($name, $checkVar)) { return true; } @@ -337,44 +338,17 @@ class Component extends \yii\base\Object public function hasEventHandlers($name) { $this->ensureBehaviors(); - return isset($this->_e[$name]) && $this->_e[$name]->getCount(); - } - - /** - * Returns the list of attached event handlers for an event. - * You may manipulate the returned [[Vector]] object by adding or removing handlers. - * For example, - * - * ~~~ - * $component->getEventHandlers($eventName)->insertAt(0, $eventHandler); - * ~~~ - * - * @param string $name the event name - * @return Vector list of attached event handlers for the event - */ - public function getEventHandlers($name) - { - if (!isset($this->_e[$name])) { - $this->_e[$name] = new Vector; - } - $this->ensureBehaviors(); - return $this->_e[$name]; + return !empty($this->_events[$name]); } /** * Attaches an event handler to an event. * - * This is equivalent to the following code: - * - * ~~~ - * $component->getEventHandlers($eventName)->add($eventHandler); - * ~~~ - * * An event handler must be a valid PHP callback. The followings are * some examples: * * ~~~ - * function($event) { ... } // anonymous function + * function ($event) { ... } // anonymous function * array($object, 'handleClick') // $object->handleClick() * array('Page', 'handleClick') // Page::handleClick() * 'handleClick' // global function handleClick() @@ -383,31 +357,53 @@ class Component extends \yii\base\Object * An event handler must be defined with the following signature, * * ~~~ - * function handlerName($event) {} + * function ($event) * ~~~ * * where `$event` is an [[Event]] object which includes parameters associated with the event. * * @param string $name the event name - * @param string|array|\Closure $handler the event handler + * @param callback $handler the event handler + * @param mixed $data the data to be passed to the event handler when the event is triggered. + * When the event handler is invoked, this data can be accessed via [[Event::data]]. * @see off() */ - public function on($name, $handler) + public function on($name, $handler, $data = null) { - $this->getEventHandlers($name)->add($handler); + $this->ensureBehaviors(); + $this->_events[$name][] = array($handler, $data); } /** * Detaches an existing event handler from this component. * This method is the opposite of [[on()]]. * @param string $name event name - * @param string|array|\Closure $handler the event handler to be removed + * @param callback $handler the event handler to be removed. + * If it is null, all handlers attached to the named event will be removed. * @return boolean if a handler is found and detached * @see on() */ - public function off($name, $handler) + public function off($name, $handler = null) { - return $this->getEventHandlers($name)->remove($handler) !== false; + $this->ensureBehaviors(); + if (isset($this->_events[$name])) { + if ($handler === null) { + $this->_events[$name] = array(); + } else { + $removed = false; + foreach ($this->_events[$name] as $i => $event) { + if ($event[0] === $handler) { + unset($this->_events[$name][$i]); + $removed = true; + } + } + if ($removed) { + $this->_events[$name] = array_values($this->_events[$name]); + } + return $removed; + } + } + return false; } /** @@ -420,7 +416,7 @@ class Component extends \yii\base\Object public function trigger($name, $event = null) { $this->ensureBehaviors(); - if (isset($this->_e[$name]) && $this->_e[$name]->getCount()) { + if (!empty($this->_events[$name])) { if ($event === null) { $event = new Event; } @@ -429,8 +425,9 @@ class Component extends \yii\base\Object } $event->handled = false; $event->name = $name; - foreach ($this->_e[$name] as $handler) { - call_user_func($handler, $event); + foreach ($this->_events[$name] as $handler) { + $event->data = $handler[1]; + call_user_func($handler[0], $event); // stop further handling if the event is handled if ($event instanceof Event && $event->handled) { return; @@ -447,7 +444,7 @@ class Component extends \yii\base\Object public function getBehavior($name) { $this->ensureBehaviors(); - return isset($this->_b[$name]) ? $this->_b[$name] : null; + return isset($this->_behaviors[$name]) ? $this->_behaviors[$name] : null; } /** @@ -457,20 +454,20 @@ class Component extends \yii\base\Object public function getBehaviors() { $this->ensureBehaviors(); - return $this->_b; + return $this->_behaviors; } /** * Attaches a behavior to this component. * This method will create the behavior object based on the given * configuration. After that, the behavior object will be attached to - * this component by calling the [[Behavior::attach]] method. + * this component by calling the [[Behavior::attach()]] method. * @param string $name the name of the behavior. * @param string|array|Behavior $behavior the behavior configuration. This can be one of the following: * * - a [[Behavior]] object * - a string specifying the behavior class - * - an object configuration array that will be passed to [[\Yii::createObject()]] to create the behavior object. + * - an object configuration array that will be passed to [[Yii::createObject()]] to create the behavior object. * * @return Behavior the behavior object * @see detachBehavior @@ -498,15 +495,15 @@ class Component extends \yii\base\Object /** * Detaches a behavior from the component. - * The behavior's [[Behavior::detach]] method will be invoked. + * The behavior's [[Behavior::detach()]] method will be invoked. * @param string $name the behavior's name. * @return Behavior the detached behavior. Null if the behavior does not exist. */ public function detachBehavior($name) { - if (isset($this->_b[$name])) { - $behavior = $this->_b[$name]; - unset($this->_b[$name]); + if (isset($this->_behaviors[$name])) { + $behavior = $this->_behaviors[$name]; + unset($this->_behaviors[$name]); $behavior->detach(); return $behavior; } else { @@ -519,12 +516,12 @@ class Component extends \yii\base\Object */ public function detachBehaviors() { - if ($this->_b !== null) { - foreach ($this->_b as $name => $behavior) { + if ($this->_behaviors !== null) { + foreach ($this->_behaviors as $name => $behavior) { $this->detachBehavior($name); } } - $this->_b = array(); + $this->_behaviors = array(); } /** @@ -532,8 +529,8 @@ class Component extends \yii\base\Object */ public function ensureBehaviors() { - if ($this->_b === null) { - $this->_b = array(); + if ($this->_behaviors === null) { + $this->_behaviors = array(); foreach ($this->behaviors() as $name => $behavior) { $this->attachBehaviorInternal($name, $behavior); } @@ -549,12 +546,12 @@ class Component extends \yii\base\Object private function attachBehaviorInternal($name, $behavior) { if (!($behavior instanceof Behavior)) { - $behavior = \Yii::createObject($behavior); + $behavior = Yii::createObject($behavior); } - if (isset($this->_b[$name])) { - $this->_b[$name]->detach(); + if (isset($this->_behaviors[$name])) { + $this->_behaviors[$name]->detach(); } $behavior->attach($this); - return $this->_b[$name] = $behavior; + return $this->_behaviors[$name] = $behavior; } } diff --git a/framework/base/Event.php b/framework/base/Event.php index b86ed7c..5d40736 100644 --- a/framework/base/Event.php +++ b/framework/base/Event.php @@ -15,12 +15,14 @@ namespace yii\base; * And the [[handled]] property indicates if the event is handled. * If an event handler sets [[handled]] to be true, the rest of the * uninvoked handlers will no longer be called to handle the event. - * Additionally, an event may specify extra parameters via the [[data]] property. + * + * Additionally, when attaching an event handler, extra data may be passed + * and be available via the [[data]] property when the event handler is invoked. * * @author Qiang Xue * @since 2.0 */ -class Event extends \yii\base\Object +class Event extends Object { /** * @var string the event name. This property is set by [[Component::trigger()]]. @@ -39,7 +41,8 @@ class Event extends \yii\base\Object */ public $handled = false; /** - * @var mixed extra custom data associated with the event. + * @var mixed the data that is passed to [[Component::on()]] when attaching an event handler. + * Note that this varies according to which event handler is currently executing. */ public $data; } diff --git a/tests/unit/framework/base/ComponentTest.php b/tests/unit/framework/base/ComponentTest.php index 97b0116..74b6e9a 100644 --- a/tests/unit/framework/base/ComponentTest.php +++ b/tests/unit/framework/base/ComponentTest.php @@ -41,12 +41,12 @@ class ComponentTest extends TestCase $component->attachBehavior('a', $behavior); $this->assertSame($behavior, $component->getBehavior('a')); $component->on('test', 'fake'); - $this->assertEquals(1, $component->getEventHandlers('test')->count); + $this->assertTrue($component->hasEventHandlers('test')); $clone = clone $component; $this->assertNotSame($component, $clone); $this->assertNull($clone->getBehavior('a')); - $this->assertEquals(0, $clone->getEventHandlers('test')->count); + $this->assertFalse($clone->hasEventHandlers('test')); } public function testHasProperty() @@ -151,34 +151,32 @@ class ComponentTest extends TestCase public function testOn() { - $this->assertEquals(0, $this->component->getEventHandlers('click')->getCount()); + $this->assertFalse($this->component->hasEventHandlers('click')); $this->component->on('click', 'foo'); - $this->assertEquals(1, $this->component->getEventHandlers('click')->getCount()); - $this->component->on('click', 'bar'); - $this->assertEquals(2, $this->component->getEventHandlers('click')->getCount()); - $p = 'on click'; - $this->component->$p = 'foo2'; - $this->assertEquals(3, $this->component->getEventHandlers('click')->getCount()); + $this->assertTrue($this->component->hasEventHandlers('click')); - $this->component->getEventHandlers('click')->add('test'); - $this->assertEquals(4, $this->component->getEventHandlers('click')->getCount()); + $this->assertFalse($this->component->hasEventHandlers('click2')); + $p = 'on click2'; + $this->component->$p = 'foo2'; + $this->assertTrue($this->component->hasEventHandlers('click2')); } public function testOff() { + $this->assertFalse($this->component->hasEventHandlers('click')); $this->component->on('click', 'foo'); - $this->component->on('click', array($this->component, 'myEventHandler')); - $this->assertEquals(2, $this->component->getEventHandlers('click')->getCount()); - - $result = $this->component->off('click', 'foo'); - $this->assertTrue($result); - $this->assertEquals(1, $this->component->getEventHandlers('click')->getCount()); - $result = $this->component->off('click', 'foo'); - $this->assertFalse($result); - $this->assertEquals(1, $this->component->getEventHandlers('click')->getCount()); - $result = $this->component->off('click', array($this->component, 'myEventHandler')); - $this->assertTrue($result); - $this->assertEquals(0, $this->component->getEventHandlers('click')->getCount()); + $this->assertTrue($this->component->hasEventHandlers('click')); + $this->component->off('click', 'foo'); + $this->assertFalse($this->component->hasEventHandlers('click')); + + $this->component->on('click2', 'foo'); + $this->component->on('click2', 'foo2'); + $this->component->on('click2', 'foo3'); + $this->assertTrue($this->component->hasEventHandlers('click2')); + $this->component->off('click2', 'foo3'); + $this->assertTrue($this->component->hasEventHandlers('click2')); + $this->component->off('click2'); + $this->assertFalse($this->component->hasEventHandlers('click2')); } public function testTrigger() From ad4cf9e8fa36b65ffaebd30dfdebf530cf3a2d90 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Fri, 5 Apr 2013 21:12:49 -0400 Subject: [PATCH 037/104] Added beforeRender and afterRender to View. --- framework/base/ActionEvent.php | 4 ++- framework/base/View.php | 55 ++++++++++++++++++++++++++++++++++++++---- framework/base/ViewEvent.php | 44 +++++++++++++++++++++++++++++++++ 3 files changed, 97 insertions(+), 6 deletions(-) create mode 100644 framework/base/ViewEvent.php diff --git a/framework/base/ActionEvent.php b/framework/base/ActionEvent.php index 7c5a40c..9507b12 100644 --- a/framework/base/ActionEvent.php +++ b/framework/base/ActionEvent.php @@ -22,7 +22,9 @@ class ActionEvent extends Event */ public $action; /** - * @var boolean whether to continue running the action. + * @var boolean whether to continue running the action. Event handlers of + * [[Controller::EVENT_BEFORE_ACTION]] may set this property to decide whether + * to continue running the current action. */ public $isValid = true; diff --git a/framework/base/View.php b/framework/base/View.php index d3d9339..9f382c6 100644 --- a/framework/base/View.php +++ b/framework/base/View.php @@ -22,6 +22,15 @@ use yii\helpers\FileHelper; class View extends Component { /** + * @event Event an event that is triggered by [[renderFile()]] right before it renders a view file. + */ + const EVENT_BEFORE_RENDER = 'beforeRender'; + /** + * @event Event an event that is triggered by [[renderFile()]] right after it renders a view file. + */ + const EVENT_AFTER_RENDER = 'afterRender'; + + /** * @var object the object that owns this view. This can be a controller, a widget, or any other object. */ public $context; @@ -47,7 +56,7 @@ class View extends Component public $clips; /** * @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. + * is maintained by [[beginWidget()]] and [[endWidget()]] methods. Do not modify it. */ public $widgetStack = array(); /** @@ -140,10 +149,14 @@ class View extends Component $this->context = $context; } - if ($this->renderer !== null) { - $output = $this->renderer->render($this, $viewFile, $params); - } else { - $output = $this->renderPhpFile($viewFile, $params); + $output = ''; + if ($this->beforeRender($viewFile)) { + if ($this->renderer !== null) { + $output = $this->renderer->render($this, $viewFile, $params); + } else { + $output = $this->renderPhpFile($viewFile, $params); + } + $this->afterRender($viewFile, $output); } $this->context = $oldContext; @@ -152,6 +165,38 @@ class View extends Component } /** + * This method is invoked right before [[renderFile()]] renders a view file. + * The default implementation will trigger the [[EVENT_BEFORE_RENDER]] event. + * If you override this method, make sure you call the parent implementation first. + * @param string $viewFile the view file to be rendered + * @return boolean whether to continue rendering the view file. + */ + public function beforeRender($viewFile) + { + $event = new ViewEvent($viewFile); + $this->trigger(self::EVENT_BEFORE_RENDER, $event); + return $event->isValid; + } + + /** + * This method is invoked right after [[renderFile()]] renders a view file. + * The default implementation will trigger the [[EVENT_AFTER_RENDER]] event. + * If you override this method, make sure you call the parent implementation first. + * @param string $viewFile the view file to be rendered + * @param string $output the rendering result of the view file. Updates to this parameter + * will be passed back and returned by [[renderFile()]]. + */ + public function afterRender($viewFile, &$output) + { + if ($this->hasEventHandlers(self::EVENT_AFTER_RENDER)) { + $event = new ViewEvent($viewFile); + $event->output = $output; + $this->trigger(self::EVENT_AFTER_RENDER, $event); + $output = $event->output; + } + } + + /** * Renders a view file as a PHP script. * * This method treats the view file as a PHP script and includes the file. diff --git a/framework/base/ViewEvent.php b/framework/base/ViewEvent.php new file mode 100644 index 0000000..cac7be4 --- /dev/null +++ b/framework/base/ViewEvent.php @@ -0,0 +1,44 @@ + + * @since 2.0 + */ +class ViewEvent extends Event +{ + /** + * @var string the rendering result of [[View::renderFile()]]. + * Event handlers may modify this property and the modified output will be + * returned by [[View::renderFile()]]. This property is only used + * by [[View::EVENT_AFTER_RENDER]] event. + */ + public $output; + /** + * @var string the view file path that is being rendered by [[View::renderFile()]]. + */ + public $viewFile; + /** + * @var boolean whether to continue rendering the view file. Event handlers of + * [[View::EVENT_BEFORE_RENDER]] may set this property to decide whether + * to continue rendering the current view file. + */ + public $isValid = true; + + /** + * Constructor. + * @param string $viewFile the view file path that is being rendered by [[View::renderFile()]]. + * @param array $config name-value pairs that will be used to initialize the object properties + */ + public function __construct($viewFile, $config = array()) + { + $this->viewFile = $viewFile; + parent::__construct($config); + } +} \ No newline at end of file From 486ab3ff67fa909b608cc92ef19eb90c3eef085a Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Mon, 8 Apr 2013 08:30:10 -0400 Subject: [PATCH 038/104] script WIP --- framework/base/View.php | 10 ++++ framework/base/ViewContent.php | 106 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 116 insertions(+) create mode 100644 framework/base/ViewContent.php diff --git a/framework/base/View.php b/framework/base/View.php index 9f382c6..8b1f4ef 100644 --- a/framework/base/View.php +++ b/framework/base/View.php @@ -35,6 +35,10 @@ class View extends Component */ public $context; /** + * @var ViewContent + */ + public $content; + /** * @var mixed custom parameters that are shared among view templates. */ public $params; @@ -83,6 +87,11 @@ class View extends Component if (is_array($this->theme)) { $this->theme = Yii::createObject($this->theme); } + if (is_array($this->content)) { + $this->content = Yii::createObject($this->content); + } else { + $this->content = new ViewContent; + } } /** @@ -156,6 +165,7 @@ class View extends Component } else { $output = $this->renderPhpFile($viewFile, $params); } + $output = $this->content->populate($output); $this->afterRender($viewFile, $output); } diff --git a/framework/base/ViewContent.php b/framework/base/ViewContent.php new file mode 100644 index 0000000..265da84 --- /dev/null +++ b/framework/base/ViewContent.php @@ -0,0 +1,106 @@ + + * @since 2.0 + */ +class ViewContent extends Component +{ + const POS_HEAD = 1; + const POS_BEGIN = 2; + const POS_END = 3; + + public $packages; + + public $title; + public $metaTags; + public $linkTags; + public $css; + public $js; + public $cssFiles; + public $jsFiles; + + public function populate($content) + { + return $content; + } + + public function reset() + { + $this->title = null; + $this->metaTags = null; + $this->linkTags = null; + $this->css = null; + $this->js = null; + $this->cssFiles = null; + $this->jsFiles = null; + } + + public function getMetaTag($key) + { + return isset($this->metaTags[$key]) ? $this->metaTags[$key] : null; + } + + public function setMetaTag($key, $tag) + { + $this->metaTags[$key] = $tag; + } + + public function getLinkTag($key) + { + return isset($this->linkTags[$key]) ? $this->linkTags[$key] : null; + } + + public function setLinkTag($key, $tag) + { + $this->linkTags[$key] = $tag; + } + + public function getCss($key) + { + return isset($this->css[$key]) ? $this->css[$key]: null; + } + + public function setCss($key, $css) + { + $this->css[$key] = $css; + } + + public function getCssFile($key) + { + return isset($this->cssFiles[$key]) ? $this->cssFiles[$key]: null; + } + + public function setCssFile($key, $file) + { + $this->cssFiles[$key] = $file; + } + + public function getJs($key, $position = self::POS_END) + { + return isset($this->js[$position][$key]) ? $this->js[$position][$key] : null; + } + + public function setJs($key, $js, $position = self::POS_END) + { + $this->js[$position][$key] = $js; + } + + public function getJsFile($key, $position = self::POS_END) + { + return isset($this->jsFiles[$position][$key]) ? $this->jsFiles[$position][$key] : null; + } + + public function setJsFile($key, $file, $position = self::POS_END) + { + $this->jsFiles[$position][$key] = $file; + } + +} \ No newline at end of file From c146d6c5e363cc2a1e979850cd299dd8366649eb Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Tue, 9 Apr 2013 07:44:01 -0400 Subject: [PATCH 039/104] allow global configuration of classes. --- framework/YiiBase.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/framework/YiiBase.php b/framework/YiiBase.php index e81a288..606506b 100644 --- a/framework/YiiBase.php +++ b/framework/YiiBase.php @@ -371,6 +371,10 @@ class YiiBase $class = static::import($class, true); } + if (isset(self::$objectConfig[ltrim($class, '\\')])) { + $config = array_merge(self::$objectConfig[ltrim($class, '\\')], $config); + } + if (($n = func_num_args()) > 1) { /** @var $reflection \ReflectionClass */ if (isset($reflections[$class])) { @@ -531,6 +535,6 @@ class YiiBase */ public static function t($message, $params = array(), $language = null) { - return Yii::$app->getI18N()->translate($message, $params, $language); + return self::$app->getI18N()->translate($message, $params, $language); } } From fc415b154c6035830374428cd8cd68d87ae7381f Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Tue, 9 Apr 2013 07:45:46 -0400 Subject: [PATCH 040/104] normalize class name. --- framework/YiiBase.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/framework/YiiBase.php b/framework/YiiBase.php index 606506b..3e7a7f2 100644 --- a/framework/YiiBase.php +++ b/framework/YiiBase.php @@ -371,8 +371,10 @@ class YiiBase $class = static::import($class, true); } - if (isset(self::$objectConfig[ltrim($class, '\\')])) { - $config = array_merge(self::$objectConfig[ltrim($class, '\\')], $config); + $class = ltrim($class, '\\'); + + if (isset(self::$objectConfig[$class])) { + $config = array_merge(self::$objectConfig[$class], $config); } if (($n = func_num_args()) > 1) { From 79b278c81696b365a7ed994feb097957308df67d Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Tue, 9 Apr 2013 08:38:31 -0400 Subject: [PATCH 041/104] refactored theme. --- framework/base/Theme.php | 35 +++++++++++++---------------------- 1 file changed, 13 insertions(+), 22 deletions(-) diff --git a/framework/base/Theme.php b/framework/base/Theme.php index 88ecb0a..e529a63 100644 --- a/framework/base/Theme.php +++ b/framework/base/Theme.php @@ -33,11 +33,17 @@ use yii\helpers\FileHelper; class Theme extends Component { /** - * @var string the root path of this theme. + * @var string the root path or path alias of this theme. All resources of this theme are located + * under this directory. This property must be set if [[pathMap]] is not set. * @see pathMap */ public $basePath; /** + * @var string the base URL (or path alias) for this theme. All resources of this theme are considered + * to be under this base URL. This property must be set. It is mainly used by [[getUrl()]]. + */ + public $baseUrl; + /** * @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 [[applyTo()]] when a view is trying to apply the theme. @@ -45,7 +51,6 @@ class Theme extends Component */ public $pathMap; - private $_baseUrl; /** * Initializes the theme. @@ -69,25 +74,11 @@ class Theme extends Component $paths[$from . DIRECTORY_SEPARATOR] = $to . DIRECTORY_SEPARATOR; } $this->pathMap = $paths; - } - - /** - * Returns the base URL for this theme. - * The method [[getUrl()]] will prefix this to the given URL. - * @return string the base URL for this theme. - */ - public function getBaseUrl() - { - return $this->_baseUrl; - } - - /** - * Sets the base URL for this theme. - * @param string $value the base URL for this theme. - */ - public function setBaseUrl($value) - { - $this->_baseUrl = rtrim(Yii::getAlias($value), '/'); + if ($this->baseUrl === null) { + throw new InvalidConfigException("Theme::baseUrl must be set."); + } else { + $this->baseUrl = rtrim(Yii::getAlias($this->baseUrl), '/'); + } } /** @@ -112,7 +103,7 @@ class Theme extends Component } /** - * Converts a relative URL into an absolute URL using [[basePath]]. + * Converts a relative URL into an absolute URL using [[baseUrl]]. * @param string $url the relative URL to be converted. * @return string the absolute URL */ From 1df9114b9a763c54d64e0fe3ef05163ae980bc29 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Tue, 9 Apr 2013 22:28:13 -0400 Subject: [PATCH 042/104] refactored ErrorException handling. --- framework/base/Application.php | 59 --------------------------------------- framework/base/ErrorException.php | 54 +++++++++++++++++++++++++---------- 2 files changed, 40 insertions(+), 73 deletions(-) diff --git a/framework/base/Application.php b/framework/base/Application.php index 9be1939..1e02c1d 100644 --- a/framework/base/Application.php +++ b/framework/base/Application.php @@ -87,9 +87,6 @@ class Application extends Module */ public $layout = 'main'; - // todo - public $localeDataPath = '@yii/i18n/data'; - private $_runtimePath; private $_ended = false; @@ -165,31 +162,6 @@ class Application extends Module if (ErrorException::isFatalErorr($error)) { unset($this->_memoryReserve); $exception = new ErrorException($error['message'], $error['type'], $error['type'], $error['file'], $error['line']); - - if (function_exists('xdebug_get_function_stack')) { - $trace = array_slice(array_reverse(xdebug_get_function_stack()), 4, -1); - foreach ($trace as &$frame) { - if (!isset($frame['function'])) { - $frame['function'] = 'unknown'; - } - - // XDebug < 2.1.1: http://bugs.xdebug.org/view.php?id=695 - if (!isset($frame['type'])) { - $frame['type'] = '::'; - } - - // XDebug has a different key name - $frame['args'] = array(); - if (isset($frame['params']) && !isset($frame['args'])) { - $frame['args'] = $frame['params']; - } - } - - $ref = new \ReflectionProperty('Exception', 'trace'); - $ref->setAccessible(true); - $ref->setValue($exception, $trace); - } - $this->logException($exception); if (($handler = $this->getErrorHandler()) !== null) { @@ -295,37 +267,6 @@ class Application extends Module date_default_timezone_set($value); } - // - // /** - // * Returns the locale instance. - // * @param string $localeID the locale ID (e.g. en_US). If null, the {@link getLanguage application language ID} will be used. - // * @return CLocale the locale instance - // */ - // public function getLocale($localeID = null) - // { - // return CLocale::getInstance($localeID === null ? $this->getLanguage() : $localeID); - // } - // - // /** - // * @return CNumberFormatter the locale-dependent number formatter. - // * The current {@link getLocale application locale} will be used. - // */ - // public function getNumberFormatter() - // { - // return $this->getLocale()->getNumberFormatter(); - // } - // - // /** - // * Returns the locale-dependent date formatter. - // * @return CDateFormatter the locale-dependent date formatter. - // * The current {@link getLocale application locale} will be used. - // */ - // public function getDateFormatter() - // { - // return $this->getLocale()->getDateFormatter(); - // } - // - /** * Returns the database connection component. * @return \yii\db\Connection the database connection diff --git a/framework/base/ErrorException.php b/framework/base/ErrorException.php index 465d839..740eea0 100644 --- a/framework/base/ErrorException.php +++ b/framework/base/ErrorException.php @@ -7,6 +7,8 @@ namespace yii\base; +use Yii; + /** * ErrorException represents a PHP error. * @@ -33,6 +35,30 @@ class ErrorException extends Exception $this->severity = $severity; $this->file = $filename; $this->line = $lineno; + + if (function_exists('xdebug_get_function_stack')) { + $trace = array_slice(array_reverse(xdebug_get_function_stack()), 3, -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($this, $trace); + } } /** @@ -62,20 +88,20 @@ class ErrorException extends 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'), + 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'); + return isset($names[$this->getCode()]) ? $names[$this->getCode()] : Yii::t('yii|Error'); } } From df94d6aa30c395f9c1ed35d78c65e1e212ee845f Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Tue, 9 Apr 2013 22:28:33 -0400 Subject: [PATCH 043/104] refactored FileTarget. --- framework/logging/FileTarget.php | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/framework/logging/FileTarget.php b/framework/logging/FileTarget.php index c3f4031..69799cd 100644 --- a/framework/logging/FileTarget.php +++ b/framework/logging/FileTarget.php @@ -6,6 +6,8 @@ */ namespace yii\logging; + +use Yii; use yii\base\InvalidConfigException; /** @@ -23,15 +25,14 @@ use yii\base\InvalidConfigException; class FileTarget extends Target { /** - * @var string log file path or path alias. If not set, it means the 'application.log' file under - * the application runtime directory. Please make sure the directory containing - * the log file is writable by the Web server process. + * @var string log file path or path alias. If not set, it will use the "runtime/logs/app.log" file. + * The directory containing the log files will be automatically created if not existing. */ public $logFile; /** - * @var integer maximum log file size, in kilo-bytes. Defaults to 1024, meaning 1MB. + * @var integer maximum log file size, in kilo-bytes. Defaults to 10240, meaning 10MB. */ - public $maxFileSize = 1024; // in KB + public $maxFileSize = 10240; // in KB /** * @var integer number of log files used for rotation. Defaults to 5. */ @@ -46,13 +47,13 @@ class FileTarget extends Target { parent::init(); if ($this->logFile === null) { - $this->logFile = \Yii::$app->getRuntimePath() . DIRECTORY_SEPARATOR . 'application.log'; + $this->logFile = Yii::$app->getRuntimePath() . '/logs/app.log'; } else { - $this->logFile = \Yii::getAlias($this->logFile); + $this->logFile = Yii::getAlias($this->logFile); } $logPath = dirname($this->logFile); - if (!is_dir($logPath) || !is_writable($logPath)) { - throw new InvalidConfigException("Directory '$logPath' does not exist or is not writable."); + if (!is_dir($logPath)) { + @mkdir($logPath, 0777, true); } if ($this->maxLogFiles < 1) { $this->maxLogFiles = 1; @@ -66,6 +67,7 @@ class FileTarget extends Target * Sends log messages to specified email addresses. * @param array $messages the messages to be exported. See [[Logger::messages]] for the structure * of each message. + * @throws InvalidConfigException if unable to open the log file for writing */ public function export($messages) { @@ -73,7 +75,9 @@ class FileTarget extends Target foreach ($messages as $message) { $text .= $this->formatMessage($message); } - $fp = @fopen($this->logFile, 'a'); + if (($fp = @fopen($this->logFile, 'a')) === false) { + throw new InvalidConfigException("Unable to append to log file: {$this->logFile}"); + } @flock($fp, LOCK_EX); if (@filesize($this->logFile) > $this->maxFileSize * 1024) { $this->rotateFiles(); From 6f11f9e349ab86c9659fc199b175e49520061d7c Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Tue, 9 Apr 2013 22:28:56 -0400 Subject: [PATCH 044/104] script WIP --- framework/base/ViewContent.php | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/framework/base/ViewContent.php b/framework/base/ViewContent.php index 265da84..6a3b489 100644 --- a/framework/base/ViewContent.php +++ b/framework/base/ViewContent.php @@ -17,7 +17,32 @@ class ViewContent extends Component const POS_BEGIN = 2; const POS_END = 3; - public $packages; + /** + * @var array + * + * Each asset bundle should be declared with the following structure: + * + * ~~~ + * array( + * 'basePath' => '...', + * 'baseUrl' => '...', // if missing, the bundle will be published to the "www/assets" folder + * 'js' => array( + * 'js/main.js', + * 'js/menu.js', + * 'js/base.js' => self::POS_HEAD, + * 'css' => array( + * 'css/main.css', + * 'css/menu.css', + * ), + * 'depends' => array( + * 'jquery', + * 'yii', + * 'yii/treeview', + * ), + * ) + * ~~~ + */ + public $bundles; public $title; public $metaTags; @@ -43,6 +68,11 @@ class ViewContent extends Component $this->jsFiles = null; } + public function registerBundle($name) + { + + } + public function getMetaTag($key) { return isset($this->metaTags[$key]) ? $this->metaTags[$key] : null; From 122bd231b4fceef9aee94b7a859ecff8acc40b09 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Tue, 9 Apr 2013 22:43:04 -0400 Subject: [PATCH 045/104] modified Application constructor signature. --- framework/base/Application.php | 22 +++++++++++++++------- tests/unit/framework/helpers/HtmlTest.php | 4 +++- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/framework/base/Application.php b/framework/base/Application.php index 1e02c1d..01e6df8 100644 --- a/framework/base/Application.php +++ b/framework/base/Application.php @@ -98,16 +98,24 @@ class Application extends Module /** * 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 - * the directory containing all application logic, template and data. - * @param array $config name-value pairs that will be used to initialize the object properties + * @param array $config name-value pairs that will be used to initialize the object properties. + * Note that the configuration must contain both [[id]] and [[basePath]]. + * @throws InvalidConfigException if either [[id]] or [[basePath]] configuration is missing. */ - public function __construct($id, $basePath, $config = array()) + public function __construct($config = array()) { Yii::$app = $this; - $this->id = $id; - $this->setBasePath($basePath); + + if (!isset($config['id'])) { + throw new InvalidConfigException('The "id" configuration is required.'); + } + + if (isset($config['basePath'])) { + $this->setBasePath($config['basePath']); + unset($config['basePath']); + } else { + throw new InvalidConfigException('The "basePath" configuration is required.'); + } if (YII_ENABLE_ERROR_HANDLER) { ini_set('display_errors', 0); diff --git a/tests/unit/framework/helpers/HtmlTest.php b/tests/unit/framework/helpers/HtmlTest.php index 2c3de72..bf0ca0a 100644 --- a/tests/unit/framework/helpers/HtmlTest.php +++ b/tests/unit/framework/helpers/HtmlTest.php @@ -10,7 +10,9 @@ class HtmlTest extends \yii\test\TestCase { public function setUp() { - new Application('test', '@yiiunit/runtime', array( + new Application(array( + 'id' => 'test', + 'basePath' => '@yiiunit/runtime', 'components' => array( 'request' => array( 'class' => 'yii\web\Request', From 55e8db9b978e23f5ac30978af53cd42e3fdc1116 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Wed, 10 Apr 2013 10:48:13 -0400 Subject: [PATCH 046/104] improved doc. --- docs/api/base/Component.md | 2 ++ docs/api/base/Object.md | 30 +++++++++++++++++++++++++++++- framework/base/Component.php | 5 ----- framework/base/Object.php | 3 --- 4 files changed, 31 insertions(+), 9 deletions(-) diff --git a/docs/api/base/Component.md b/docs/api/base/Component.md index b5a07fd..01ef462 100644 --- a/docs/api/base/Component.md +++ b/docs/api/base/Component.md @@ -1,3 +1,5 @@ +Component is the base class that implements the *property*, *event* and *behavior* features. + Component provides the *event* and *behavior* features, in addition to the *property* feature which is implemented in its parent class [[Object]]. diff --git a/docs/api/base/Object.md b/docs/api/base/Object.md index a2cea6c..1b9fca0 100644 --- a/docs/api/base/Object.md +++ b/docs/api/base/Object.md @@ -1,3 +1,5 @@ +Object is the base class that implements the *property* feature. + A property is defined by a getter method (e.g. `getLabel`), and/or a setter method (e.g. `setLabel`). For example, the following getter and setter methods define a property named `label`: @@ -30,4 +32,30 @@ $object->label = 'abc'; If a property has only a getter method and has no setter method, it is considered as *read-only*. In this case, trying to modify the property value will cause an exception. -One can call [[hasProperty]], [[canGetProperty]] and/or [[canSetProperty]] to check the existence of a property. +One can call [[hasProperty()]], [[canGetProperty()]] and/or [[canSetProperty()]] to check the existence of a property. + + +Besides the property feature, Object also introduces an important object initialization life cycle. In particular, +creating an new instance of Object or its derived class will involve the following life cycles sequentially: + +1. the class constructor is invoked; +2. object properties are initialized according to the given configuration; +3. the `init()` method is invoked. + +In the above, both Step 2 and 3 occur at the end of the class constructor. It is recommended that +you perform object initialization in the `init()` method because at that stage, the object configuration +is already applied. + +In order to ensure the above life cycles, if a child class of Object needs to override the constructor, +it should be done like the following: + +~~~ +public function __construct($param1, $param2, ..., $config = array()) +{ + ... + parent::__construct($config); +} +~~~ + +That is, a `$config` parameter (defaults to `array()`) should be declared as the last parameter +of the constructor, and the parent implementation should be called at the end of the constructor. diff --git a/framework/base/Component.php b/framework/base/Component.php index 5a76ee3..80259e7 100644 --- a/framework/base/Component.php +++ b/framework/base/Component.php @@ -10,12 +10,7 @@ namespace yii\base; use Yii; /** - * Component is the base class that provides the *property*, *event* and *behavior* features. - * * @include @yii/base/Component.md - * - * @property Behavior[] behaviors list of behaviors currently attached to this component - * * @author Qiang Xue * @since 2.0 */ diff --git a/framework/base/Object.php b/framework/base/Object.php index 3bd8378..a547990 100644 --- a/framework/base/Object.php +++ b/framework/base/Object.php @@ -8,10 +8,7 @@ namespace yii\base; /** - * Object is the base class that provides the *property* feature. - * * @include @yii/base/Object.md - * * @author Qiang Xue * @since 2.0 */ From 4c2fcd76082dfc3511efdce1898c846460403099 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Wed, 10 Apr 2013 10:48:27 -0400 Subject: [PATCH 047/104] refactored application. --- framework/base/Application.php | 30 ++++++++++-------------------- framework/base/Module.php | 1 - framework/web/Application.php | 14 ++++---------- framework/web/Request.php | 33 ++++++++++++++++++++++++++++----- 4 files changed, 42 insertions(+), 36 deletions(-) diff --git a/framework/base/Application.php b/framework/base/Application.php index 01e6df8..6b0dfa3 100644 --- a/framework/base/Application.php +++ b/framework/base/Application.php @@ -113,29 +113,27 @@ class Application extends Module if (isset($config['basePath'])) { $this->setBasePath($config['basePath']); unset($config['basePath']); + Yii::$aliases['@app'] = $this->getBasePath(); } else { throw new InvalidConfigException('The "basePath" configuration is required.'); } - if (YII_ENABLE_ERROR_HANDLER) { - ini_set('display_errors', 0); - set_exception_handler(array($this, 'handleException')); - set_error_handler(array($this, 'handleError'), error_reporting()); - } - - $this->registerDefaultAliases(); + $this->registerErrorHandlers(); $this->registerCoreComponents(); - Component::__construct($config); + parent::__construct($config); } /** - * Initializes the application by loading components declared in [[preload]]. - * If you override this method, make sure the parent implementation is invoked. + * Registers error handlers. */ - public function init() + public function registerErrorHandlers() { - $this->preloadComponents(); + if (YII_ENABLE_ERROR_HANDLER) { + ini_set('display_errors', 0); + set_exception_handler(array($this, 'handleException')); + set_error_handler(array($this, 'handleError'), error_reporting()); + } } /** @@ -339,14 +337,6 @@ class Application extends Module } /** - * Sets default path aliases. - */ - public function registerDefaultAliases() - { - Yii::$aliases['@app'] = $this->getBasePath(); - } - - /** * Registers the core application components. * @see setComponents */ diff --git a/framework/base/Module.php b/framework/base/Module.php index 296494d..e2fc1b5 100644 --- a/framework/base/Module.php +++ b/framework/base/Module.php @@ -170,7 +170,6 @@ abstract class Module extends Component */ public function init() { - Yii::setAlias('@' . $this->id, $this->getBasePath()); $this->preloadComponents(); } diff --git a/framework/web/Application.php b/framework/web/Application.php index b839d92..f9b615d 100644 --- a/framework/web/Application.php +++ b/framework/web/Application.php @@ -23,21 +23,15 @@ class Application extends \yii\base\Application public $defaultRoute = 'site'; /** - * Sets default path aliases. - */ - public function registerDefaultAliases() - { - parent::registerDefaultAliases(); - Yii::$aliases['@webroot'] = dirname($_SERVER['SCRIPT_FILENAME']); - } - - /** * Processes the request. * @return integer the exit status of the controller action (0 means normal, non-zero values mean abnormal) */ public function processRequest() { - list ($route, $params) = $this->getRequest()->resolve(); + $request = $this->getRequest(); + Yii::setAlias('@wwwroot', dirname($request->getScriptFile())); + Yii::setAlias('@www', $request->getBaseUrl()); + list ($route, $params) = $request->resolve(); return $this->runAction($route, $params); } diff --git a/framework/web/Request.php b/framework/web/Request.php index 093a394..369fa0c 100644 --- a/framework/web/Request.php +++ b/framework/web/Request.php @@ -43,8 +43,6 @@ class Request extends \yii\base\Request */ public function resolve() { - Yii::setAlias('@www', $this->getBaseUrl()); - $result = Yii::$app->getUrlManager()->parseRequest($this); if ($result !== false) { list ($route, $params) = $result; @@ -301,7 +299,8 @@ class Request extends \yii\base\Request public function getScriptUrl() { if ($this->_scriptUrl === null) { - $scriptName = basename($_SERVER['SCRIPT_FILENAME']); + $scriptFile = $this->getScriptFile(); + $scriptName = basename($scriptFile); if (basename($_SERVER['SCRIPT_NAME']) === $scriptName) { $this->_scriptUrl = $_SERVER['SCRIPT_NAME']; } elseif (basename($_SERVER['PHP_SELF']) === $scriptName) { @@ -310,8 +309,8 @@ class Request extends \yii\base\Request $this->_scriptUrl = $_SERVER['ORIG_SCRIPT_NAME']; } elseif (($pos = strpos($_SERVER['PHP_SELF'], '/' . $scriptName)) !== false) { $this->_scriptUrl = substr($_SERVER['SCRIPT_NAME'], 0, $pos) . '/' . $scriptName; - } elseif (isset($_SERVER['DOCUMENT_ROOT']) && strpos($_SERVER['SCRIPT_FILENAME'], $_SERVER['DOCUMENT_ROOT']) === 0) { - $this->_scriptUrl = str_replace('\\', '/', str_replace($_SERVER['DOCUMENT_ROOT'], '', $_SERVER['SCRIPT_FILENAME'])); + } elseif (isset($_SERVER['DOCUMENT_ROOT']) && strpos($scriptFile, $_SERVER['DOCUMENT_ROOT']) === 0) { + $this->_scriptUrl = str_replace('\\', '/', str_replace($_SERVER['DOCUMENT_ROOT'], '', $scriptFile)); } else { throw new InvalidConfigException('Unable to determine the entry script URL.'); } @@ -330,6 +329,30 @@ class Request extends \yii\base\Request $this->_scriptUrl = '/' . trim($value, '/'); } + private $_scriptFile; + + /** + * Returns the entry script file path. + * The default implementation will simply return `$_SERVER['SCRIPT_FILENAME']`. + * @return string the entry script file path + */ + public function getScriptFile() + { + return isset($this->_scriptFile) ? $this->_scriptFile : $_SERVER['SCRIPT_FILENAME']; + } + + /** + * Sets the entry script file path. + * The entry script file path normally can be obtained from `$_SERVER['SCRIPT_FILENAME']`. + * If your server configuration does not return the correct value, you may configure + * this property to make it right. + * @param string $value the entry script file path. + */ + public function setScriptFile($value) + { + $this->_scriptFile = $value; + } + private $_pathInfo; /** From e2864ca522ef9d24db950fcdec4315dd557ab773 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Wed, 10 Apr 2013 21:53:11 -0400 Subject: [PATCH 048/104] refactoring autoloading (WIP) --- framework/YiiBase.php | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/framework/YiiBase.php b/framework/YiiBase.php index 3e7a7f2..a41b591 100644 --- a/framework/YiiBase.php +++ b/framework/YiiBase.php @@ -46,8 +46,8 @@ class YiiBase { /** * @var array class map used by the Yii autoloading mechanism. - * The array keys are the class names, and the array values are the corresponding class file paths. - * This property mainly affects how [[autoload]] works. + * The array keys are the class names (without leading backslashes), and the array values + * are the corresponding class file paths. This property mainly affects how [[autoload()]] works. * @see import * @see autoload */ @@ -113,7 +113,7 @@ class YiiBase * includes the class file when the class is referenced in the code the first time. * * Importing a directory will add the directory to the front of the [[classPath]] array. - * When [[autoload]] is loading an unknown class, it will search in the directories + * When [[autoload()]] is loading an unknown class, it will search in the directories * specified in [[classPath]] to find the corresponding class file to include. * For this reason, if multiple directories are imported, the directories imported later * will take precedence in class file searching. @@ -273,7 +273,7 @@ class YiiBase return true; } - if (strpos($className, '\\') !== false) { + if (strrpos($className, '\\') > 0) { // 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, '\\')); @@ -296,17 +296,19 @@ class YiiBase if (is_file($path)) { $classFile = $path; $alias = $className; + break; } } } if (isset($classFile, $alias) && is_file($classFile)) { - if (!YII_DEBUG || basename(realpath($classFile)) === basename($alias) . '.php') { + if (basename(realpath($classFile)) === basename($alias) . '.php') { include($classFile); - return true; - } else { - throw new Exception("Class name '$className' does not match the class file '" . realpath($classFile) . "'. Have you checked their case sensitivity?"); + if (class_exists($className, false)) { + return true; + } } + throw new Exception("The class file name '" . realpath($classFile) . "' does not match the class name '$className'. Please check the case of the names and make sure the class file does not have syntax errors."); } return false; From f2027c1cddd2bceb35d4cbbc6543e0e40b3203f8 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Wed, 10 Apr 2013 23:36:44 -0400 Subject: [PATCH 049/104] Fixed errorexception trace type. --- framework/base/ErrorException.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/framework/base/ErrorException.php b/framework/base/ErrorException.php index 740eea0..93390e7 100644 --- a/framework/base/ErrorException.php +++ b/framework/base/ErrorException.php @@ -44,8 +44,10 @@ class ErrorException extends Exception } // XDebug < 2.1.1: http://bugs.xdebug.org/view.php?id=695 - if (!isset($frame['type'])) { + if (!isset($frame['type']) || $frame['type'] === 'static') { $frame['type'] = '::'; + } elseif ($frame['type'] === 'dynamic') { + $frame['type'] = '->'; } // XDebug has a different key name From 6ae715ff8a416ea4bee2f8e739de51e0f5bccf99 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Wed, 10 Apr 2013 23:39:36 -0400 Subject: [PATCH 050/104] refactored autoloading. --- framework/YiiBase.php | 71 +++++++++++++++++------------------------- framework/base/Application.php | 2 +- framework/base/Module.php | 19 ++++------- 3 files changed, 36 insertions(+), 56 deletions(-) diff --git a/framework/YiiBase.php b/framework/YiiBase.php index a41b591..6df998b 100644 --- a/framework/YiiBase.php +++ b/framework/YiiBase.php @@ -47,7 +47,8 @@ class YiiBase /** * @var array class map used by the Yii autoloading mechanism. * The array keys are the class names (without leading backslashes), and the array values - * are the corresponding class file paths. This property mainly affects how [[autoload()]] works. + * are the corresponding class file paths (or path aliases). This property mainly affects + * how [[autoload()]] works. * @see import * @see autoload */ @@ -188,10 +189,10 @@ class YiiBase * it will be returned back without change. * * Note, this method does not ensure the existence of the resulting path. - * @param string $alias alias + * @param string $alias the alias to be translated. * @param boolean $throwException whether to throw an exception if the given alias is invalid. * If this is false and an invalid alias is given, false will be returned by this method. - * @return string|boolean path corresponding to the alias, false if the root alias is not previously registered. + * @return string|boolean the path corresponding to the alias, false if the root alias is not previously registered. * @throws InvalidParamException if the alias is invalid while $throwException is true. * @see setAlias */ @@ -225,13 +226,14 @@ class YiiBase * Note that this method neither checks the existence of the path nor normalizes the path. * Any trailing '/' and '\' characters in the path will be trimmed. * - * @param string $alias alias to the path. The alias must start with '@'. + * @param string $alias the alias name (e.g. "@yii"). It should start with a '@' character + * and should NOT contain the forward slash "/" or the backward slash "\". * @param string $path the path corresponding to the alias. This can be * * - a directory or a file path (e.g. `/tmp`, `/tmp/main.txt`) * - a URL (e.g. `http://www.yiiframework.com`) * - a path alias (e.g. `@yii/base`). In this case, the path alias will be converted into the - * actual path first by calling [[getAlias]]. + * actual path first by calling [[getAlias()]]. * * @throws Exception if $path is an invalid alias * @see getAlias @@ -268,50 +270,35 @@ class YiiBase */ public static function autoload($className) { - if (isset(self::$classMap[$className])) { - include(self::$classMap[$className]); - return true; - } + $className = ltrim($className, '\\'); - if (strrpos($className, '\\') > 0) { - // 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)) !== 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)) !== false) { - $classFile = $path . '.php'; + if (isset(self::$classMap[$className])) { + $classFile = self::$classMap[$className]; + } else { + if (($pos = strrpos($className, '\\')) !== false) { + // namespaced class, e.g. yii\base\Component + $classFile = str_replace('\\', '/', substr($className, 0, $pos + 1)) + . str_replace('_', '/', substr($className, $pos + 1)) . '.php'; + } else { + $classFile = str_replace('_', '/', $className) . '.php'; } - } - - if (!isset($classFile)) { - // search in include paths - foreach (self::$classPath as $path) { - $path .= DIRECTORY_SEPARATOR . $className . '.php'; - if (is_file($path)) { - $classFile = $path; - $alias = $className; - break; - } + if (strpos($classFile, '/') !== false) { + // make it into a path alias + $classFile = '@' . $classFile; } } - if (isset($classFile, $alias) && is_file($classFile)) { - if (basename(realpath($classFile)) === basename($alias) . '.php') { - include($classFile); - if (class_exists($className, false)) { - return true; - } + $classFile = static::getAlias($classFile); + if ($classFile !== false && is_file($classFile)) { + include($classFile); + if (class_exists($className, false) || interface_exists($className, false)) { + return true; + } else { + throw new Exception("Unable to find '$className' in file: $classFile"); } - throw new Exception("The class file name '" . realpath($classFile) . "' does not match the class name '$className'. Please check the case of the names and make sure the class file does not have syntax errors."); + } else { + return false; } - - return false; } /** diff --git a/framework/base/Application.php b/framework/base/Application.php index 6b0dfa3..e1c1d60 100644 --- a/framework/base/Application.php +++ b/framework/base/Application.php @@ -121,7 +121,7 @@ class Application extends Module $this->registerErrorHandlers(); $this->registerCoreComponents(); - parent::__construct($config); + Component::__construct($config); } /** diff --git a/framework/base/Module.php b/framework/base/Module.php index e2fc1b5..2ccf61d 100644 --- a/framework/base/Module.php +++ b/framework/base/Module.php @@ -605,21 +605,14 @@ abstract class Module extends Component $controller = Yii::createObject($this->controllerMap[$id], $id, $this); } elseif (preg_match('/^[a-z0-9\\-_]+$/', $id)) { $className = StringHelper::id2camel($id) . 'Controller'; - $classFile = $this->controllerPath . DIRECTORY_SEPARATOR . $className . '.php'; - if (is_file($classFile)) { - $className = $this->controllerNamespace . '\\' . $className; - if (!class_exists($className, false)) { - require($classFile); - } - if (class_exists($className, false) && is_subclass_of($className, '\yii\base\Controller')) { + $className = ltrim($this->controllerNamespace . '\\' . $className, '\\'); + Yii::$classMap[$className] = $classFile; + if (class_exists($className)) { + if (is_subclass_of($className, 'yii\base\Controller')) { $controller = new $className($id, $this); - } elseif (YII_DEBUG) { - if (!class_exists($className, false)) { - throw new InvalidConfigException("Class file name does not match class name: $className."); - } elseif (!is_subclass_of($className, '\yii\base\Controller')) { - throw new InvalidConfigException("Controller class must extend from \\yii\\base\\Controller."); - } + } elseif (YII_DEBUG && !is_subclass_of($className, 'yii\base\Controller')) { + throw new InvalidConfigException("Controller class must extend from \\yii\\base\\Controller."); } } } From bcc833200a9861eabf43d7ebe1d5cacf71ca1de4 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Thu, 11 Apr 2013 17:40:23 -0400 Subject: [PATCH 051/104] simplified Yii::import(). --- framework/YiiBase.php | 81 +++++++++----------------------- framework/base/Module.php | 13 ----- framework/validators/UniqueValidator.php | 2 +- 3 files changed, 23 insertions(+), 73 deletions(-) diff --git a/framework/YiiBase.php b/framework/YiiBase.php index 6df998b..7b3acee 100644 --- a/framework/YiiBase.php +++ b/framework/YiiBase.php @@ -107,73 +107,36 @@ class YiiBase } /** - * Imports a class or a directory. + * Imports a class by its alias. * - * Importing a class is like including the corresponding class file. - * The main difference is that importing a class is much lighter because it only - * includes the class file when the class is referenced in the code the first time. + * This method is provided to support autoloading of non-namespaced classes. + * Such a class can be specified in terms of an alias. For example, the alias `@old/code/Sample` + * may represent the `Sample` class under the directory `@old/code` (a path alias). * - * Importing a directory will add the directory to the front of the [[classPath]] array. - * When [[autoload()]] is loading an unknown class, it will search in the directories - * specified in [[classPath]] to find the corresponding class file to include. - * For this reason, if multiple directories are imported, the directories imported later - * will take precedence in class file searching. + * By importing a class, the class is put in an internal storage such that when + * the class is used for the first time, the class autoloader will be able to + * find the corresponding class file and include it. For this reason, this method + * is much lighter than `include()`. * - * The same class or directory can be imported multiple times. Only the first importing - * will count. Importing a directory does not import any of its subdirectories. + * You may import the same class multiple times. Only the first importing will count. * - * To import a class or a directory, one can use either path alias or class name (can be namespaced): - * - * - `@app/components/GoogleMap`: importing the `GoogleMap` class with a path alias; - * - `@app/components/*`: importing the whole `components` directory with a path alias; - * - `GoogleMap`: importing the `GoogleMap` class with a class name. [[autoload()]] will be used - * when this class is used for the first time. - * - * @param string $alias path alias or a simple class name to be imported - * @param boolean $forceInclude whether to include the class file immediately. If false, the class file - * will be included only when the class is being used. This parameter is used only when - * the path alias refers to a class. - * @return string the class name or the directory that this alias refers to - * @throws Exception if the path alias is invalid + * @param string $alias the class to be imported. This may be either a class alias or a fully-qualified class name. + * If the latter, it will be returned back without change. + * @return string the actual class name that `$alias` refers to + * @throws Exception if the alias is invalid */ - public static function import($alias, $forceInclude = false) + public static function import($alias) { - if (isset(self::$_imported[$alias])) { - return self::$_imported[$alias]; - } - - if ($alias[0] !== '@') { - // a simple class name - if (class_exists($alias, false) || interface_exists($alias, false)) { - return self::$_imported[$alias] = $alias; - } - if ($forceInclude && static::autoload($alias)) { - self::$_imported[$alias] = $alias; - } + if (strncmp($alias, '@', 1)) { return $alias; - } - - $className = basename($alias); - $isClass = $className !== '*'; - - if ($isClass && (class_exists($className, false) || interface_exists($className, false))) { - return self::$_imported[$alias] = $className; - } - - $path = static::getAlias(dirname($alias)); - - if ($isClass) { - if ($forceInclude) { - require($path . "/$className.php"); + } else { + $alias = static::getAlias($alias); + if (!isset(self::$_imported[$alias])) { + $className = basename($alias); self::$_imported[$alias] = $className; - } else { - self::$classMap[$className] = $path . DIRECTORY_SEPARATOR . "$className.php"; + self::$classMap[$className] = $alias . '.php'; } - return $className; - } else { - // a directory - array_unshift(self::$classPath, $path); - return self::$_imported[$alias] = $path; + return self::$_imported[$alias]; } } @@ -357,7 +320,7 @@ class YiiBase } if (!class_exists($class, false)) { - $class = static::import($class, true); + $class = static::import($class); } $class = ltrim($class, '\\'); diff --git a/framework/base/Module.php b/framework/base/Module.php index 2ccf61d..74c848b 100644 --- a/framework/base/Module.php +++ b/framework/base/Module.php @@ -286,19 +286,6 @@ abstract class Module extends Component } /** - * Imports the specified path aliases. - * This method is provided so that you can import a set of path aliases when configuring a module. - * The path aliases will be imported by calling [[Yii::import()]]. - * @param array $aliases list of path aliases to be imported - */ - public function setImport($aliases) - { - foreach ($aliases as $alias) { - Yii::import($alias); - } - } - - /** * Defines path aliases. * This method calls [[Yii::setAlias()]] to register the path aliases. * This method is provided so that you can define path aliases when configuring a module. diff --git a/framework/validators/UniqueValidator.php b/framework/validators/UniqueValidator.php index fa55df7..2240e0a 100644 --- a/framework/validators/UniqueValidator.php +++ b/framework/validators/UniqueValidator.php @@ -60,7 +60,7 @@ class UniqueValidator extends Validator } /** @var $className \yii\db\ActiveRecord */ - $className = $this->className === null ? get_class($object) : \Yii::import($this->className); + $className = $this->className === null ? get_class($object) : Yii::import($this->className); $attributeName = $this->attributeName === null ? $attribute : $this->attributeName; $table = $className::getTableSchema(); From f22dd82fb6426982c85d1a852cfc385494419f39 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Thu, 11 Apr 2013 20:30:55 -0400 Subject: [PATCH 052/104] refactored autoloader. --- framework/YiiBase.php | 79 +++++++++++++++++++------------ framework/base/Module.php | 13 ++--- framework/base/UnknownClassException.php | 26 ++++++++++ framework/base/UnknownMethodException.php | 2 +- framework/web/User.php | 4 +- 5 files changed, 85 insertions(+), 39 deletions(-) create mode 100644 framework/base/UnknownClassException.php diff --git a/framework/YiiBase.php b/framework/YiiBase.php index 7b3acee..ceaeda1 100644 --- a/framework/YiiBase.php +++ b/framework/YiiBase.php @@ -7,6 +7,7 @@ use yii\base\Exception; use yii\base\InvalidConfigException; use yii\base\InvalidParamException; +use yii\base\UnknownClassException; use yii\logging\Logger; /** @@ -54,13 +55,10 @@ class YiiBase */ public static $classMap = array(); /** - * @var array list of directories where Yii will search for new classes to be included. - * The first directory in the array will be searched first, and so on. - * This property mainly affects how [[autoload]] works. - * @see import - * @see autoload + * @var boolean whether to search PHP include_path when autoloading unknown classes. + * You may want to turn this off if you are also using autoloaders from other libraries. */ - public static $classPath = array(); + public static $enableIncludePath = true; /** * @var yii\console\Application|yii\web\Application the application instance */ @@ -214,8 +212,8 @@ class YiiBase /** * Class autoload loader. - * This method is invoked automatically when the execution encounters an unknown class. - * The method will attempt to include the class file as follows: + * This method is invoked automatically when PHP sees an unknown class. + * The method will attempt to include the class file according to the following procedure: * * 1. Search in [[classMap]]; * 2. If the class is namespaced (e.g. `yii\base\Component`), it will attempt @@ -224,43 +222,64 @@ class YiiBase * 3. If the class is named in PEAR style (e.g. `PHPUnit_Framework_TestCase`), * it will attempt to include the file associated with the corresponding path alias * (e.g. `@PHPUnit/Framework/TestCase.php`); - * 4. Search in [[classPath]]; + * 4. Search PHP include_path for the actual class file if [[enableIncludePath]] is true; * 5. Return false so that other autoloaders have chance to include the class file. * * @param string $className class name * @return boolean whether the class has been loaded successfully - * @throws Exception if the class file does not exist + * @throws InvalidConfigException if the class file does not exist + * @throws UnknownClassException if the class does not exist in the class file */ public static function autoload($className) { $className = ltrim($className, '\\'); if (isset(self::$classMap[$className])) { - $classFile = self::$classMap[$className]; + $classFile = static::getAlias(self::$classMap[$className]); + if (!is_file($classFile)) { + throw new InvalidConfigException("Class file does not exist: $classFile"); + } } else { + // follow PSR-0 to determine the class file if (($pos = strrpos($className, '\\')) !== false) { // namespaced class, e.g. yii\base\Component - $classFile = str_replace('\\', '/', substr($className, 0, $pos + 1)) + $path = str_replace('\\', '/', substr($className, 0, $pos + 1)) . str_replace('_', '/', substr($className, $pos + 1)) . '.php'; } else { - $classFile = str_replace('_', '/', $className) . '.php'; + $path = str_replace('_', '/', $className) . '.php'; } - if (strpos($classFile, '/') !== false) { - // make it into a path alias - $classFile = '@' . $classFile; + + // try via path alias first + if (strpos($path, '/') !== false) { + $fullPath = static::getAlias('@' . $path, false); + if ($fullPath !== false && is_file($fullPath)) { + $classFile = $fullPath; + } } - } - $classFile = static::getAlias($classFile); - if ($classFile !== false && is_file($classFile)) { - include($classFile); - if (class_exists($className, false) || interface_exists($className, false)) { - return true; - } else { - throw new Exception("Unable to find '$className' in file: $classFile"); + // search include_path + if (!isset($classFile) && self::$enableIncludePath) { + foreach (array_unique(explode(PATH_SEPARATOR, get_include_path())) as $basePath) { + $fullPath = $basePath . '/' . $path; + if (is_file($fullPath)) { + $classFile = $fullPath; + break; + } + } + } + + if (!isset($classFile)) { + // return false to let other autoloaders to try loading the class + return false; } + } + + include($classFile); + + if (class_exists($className, false) || interface_exists($className, false)) { + return true; } else { - return false; + throw new UnknownClassException("Unable to find '$className' in file: $classFile"); } } @@ -268,16 +287,16 @@ class YiiBase * Creates a new object using the given configuration. * * The configuration can be either a string or an array. - * If a string, it is treated as the *object type*; if an array, - * it must contain a `class` element specifying the *object type*, and + * If a string, it is treated as the *object class*; if an array, + * it must contain a `class` element specifying the *object class*, and * the rest of the name-value pairs in the array will be used to initialize * the corresponding object properties. * - * The object type can be either a class name or the [[getAlias|alias]] of + * The object type can be either a class name or the [[getAlias()|alias]] of * the class. For example, * - * - `\app\components\GoogleMap`: fully-qualified namespaced class. - * - `@app/components/GoogleMap`: an alias + * - `app\components\GoogleMap`: fully-qualified namespaced class. + * - `@app/components/GoogleMap`: an alias, used for non-namespaced class. * * Below are some usage examples: * diff --git a/framework/base/Module.php b/framework/base/Module.php index 74c848b..0b2bd16 100644 --- a/framework/base/Module.php +++ b/framework/base/Module.php @@ -593,14 +593,15 @@ abstract class Module extends Component } elseif (preg_match('/^[a-z0-9\\-_]+$/', $id)) { $className = StringHelper::id2camel($id) . 'Controller'; $classFile = $this->controllerPath . DIRECTORY_SEPARATOR . $className . '.php'; + if (!is_file($classFile)) { + return false; + } $className = ltrim($this->controllerNamespace . '\\' . $className, '\\'); Yii::$classMap[$className] = $classFile; - if (class_exists($className)) { - if (is_subclass_of($className, 'yii\base\Controller')) { - $controller = new $className($id, $this); - } elseif (YII_DEBUG && !is_subclass_of($className, 'yii\base\Controller')) { - throw new InvalidConfigException("Controller class must extend from \\yii\\base\\Controller."); - } + if (is_subclass_of($className, 'yii\base\Controller')) { + $controller = new $className($id, $this); + } elseif (YII_DEBUG) { + throw new InvalidConfigException("Controller class must extend from \\yii\\base\\Controller."); } } diff --git a/framework/base/UnknownClassException.php b/framework/base/UnknownClassException.php new file mode 100644 index 0000000..ac44746 --- /dev/null +++ b/framework/base/UnknownClassException.php @@ -0,0 +1,26 @@ + + * @since 2.0 + */ +class UnknownClassException extends Exception +{ + /** + * @return string the user-friendly name of this exception + */ + public function getName() + { + return \Yii::t('yii|Unknown Class'); + } +} + diff --git a/framework/base/UnknownMethodException.php b/framework/base/UnknownMethodException.php index 29bedca..440e76e 100644 --- a/framework/base/UnknownMethodException.php +++ b/framework/base/UnknownMethodException.php @@ -8,7 +8,7 @@ namespace yii\base; /** - * UnknownMethodException represents an exception caused by accessing unknown object methods. + * UnknownMethodException represents an exception caused by accessing an unknown object method. * * @author Qiang Xue * @since 2.0 diff --git a/framework/web/User.php b/framework/web/User.php index 4dc2607..435b606 100644 --- a/framework/web/User.php +++ b/framework/web/User.php @@ -32,7 +32,7 @@ class User extends Component const EVENT_AFTER_LOGOUT = 'afterLogout'; /** - * @var string the class name of the [[identity]] object. + * @var string the class name or alias of the [[identity]] object. */ public $identityClass; /** @@ -131,7 +131,7 @@ class User extends Component $this->_identity = null; } else { /** @var $class Identity */ - $class = $this->identityClass; + $class = Yii::import($this->identityClass); $this->_identity = $class::findIdentity($id); } } From 7599d7860c030788d0e2db086ae41e45cf67c5b6 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Thu, 11 Apr 2013 22:55:31 -0400 Subject: [PATCH 053/104] refactored getAlias and setAlias. --- framework/YiiBase.php | 118 ++++++++++++++++++++++++----------- framework/caching/FileCache.php | 4 +- tests/unit/framework/YiiBaseTest.php | 36 ++++++++++- 3 files changed, 116 insertions(+), 42 deletions(-) diff --git a/framework/YiiBase.php b/framework/YiiBase.php index ceaeda1..fb2967a 100644 --- a/framework/YiiBase.php +++ b/framework/YiiBase.php @@ -141,15 +141,26 @@ class YiiBase /** * Translates a path alias into an actual path. * - * The path alias can be either a root alias registered via [[setAlias]] or an - * alias starting with a root alias (e.g. `@yii/base/Component.php`). - * In the latter case, the root alias will be replaced by the corresponding registered path - * and the remaining part will be appended to it. + * The translation is done according to the following procedure: * - * In case the given parameter is not an alias (i.e., not starting with '@'), - * it will be returned back without change. + * 1. If the given alias does not start with '@', it is returned back without change; + * 2. Otherwise, look for the longest registered alias that matches the beginning part + * of the given alias. If it exists, replace the matching part of the given alias with + * the corresponding registered path. + * 3. Throw an exception or return false, depending on the `$throwException` parameter. + * + * For example, by default '@yii' is registered as the alias to the Yii framework directory, + * say '/path/to/yii'. The alias '@yii/web' would then be translated into '/path/to/yii/web'. + * + * If you have registered two aliases '@foo' and '@foo/bar'. Then translating '@foo/bar/config' + * would replace the part '@foo/bar' (instead of '@foo') with the corresponding registered path. + * This is because the longest alias takes precedence. + * + * However, if the alias to be translated is '@foo/barbar/config', then '@foo' will be replaced + * instead of '@foo/bar', because '/' serves as the boundary character. + * + * Note, this method does not check if the returned path exists or not. * - * Note, this method does not ensure the existence of the resulting path. * @param string $alias the alias to be translated. * @param boolean $throwException whether to throw an exception if the given alias is invalid. * If this is false and an invalid alias is given, false will be returned by this method. @@ -159,18 +170,26 @@ class YiiBase */ public static function getAlias($alias, $throwException = true) { - if (is_string($alias)) { - if (isset(self::$aliases[$alias])) { - return self::$aliases[$alias]; - } elseif ($alias === '' || $alias[0] !== '@') { // not an alias - return $alias; - } elseif (($pos = strpos($alias, '/')) !== false || ($pos = strpos($alias, '\\')) !== false) { - $rootAlias = substr($alias, 0, $pos); - if (isset(self::$aliases[$rootAlias])) { - return self::$aliases[$alias] = self::$aliases[$rootAlias] . substr($alias, $pos); + if (strncmp($alias, '@', 1)) { + // not an alias + return $alias; + } + + $pos = strpos($alias, '/'); + $root = $pos === false ? $alias : substr($alias, 0, $pos); + + if (isset(self::$aliases[$root])) { + if (is_string(self::$aliases[$root])) { + return $pos === false ? self::$aliases[$root] : self::$aliases[$root] . substr($alias, $pos); + } else { + foreach (self::$aliases[$root] as $name => $path) { + if (strpos($alias . '/', $name . '/') === 0) { + return $path . substr($alias, strlen($name)); + } } } } + if ($throwException) { throw new InvalidParamException("Invalid path alias: $alias"); } else { @@ -181,32 +200,61 @@ class YiiBase /** * Registers a path alias. * - * A path alias is a short name representing a path (a file path, a URL, etc.) - * A path alias must start with '@' (e.g. '@yii'). + * A path alias is a short name representing a long path (a file path, a URL, etc.) + * For example, we use '@yii' as the alias of the path to the Yii framework directory. * - * Note that this method neither checks the existence of the path nor normalizes the path. - * Any trailing '/' and '\' characters in the path will be trimmed. + * A path alias must start with the character '@' so that it can be easily differentiated + * from non-alias paths. * - * @param string $alias the alias name (e.g. "@yii"). It should start with a '@' character - * and should NOT contain the forward slash "/" or the backward slash "\". - * @param string $path the path corresponding to the alias. This can be + * Note that this method does not check if the given path exists or not. All it does is + * to associate the alias with the path. + * + * Any trailing '/' and '\' characters in the given path will be trimmed. + * + * @param string $alias the alias name (e.g. "@yii"). It must start with a '@' character. + * It may contain the forward slash '/' which serves as boundary character when performing + * alias translation by [[getAlias()]]. + * @param string $path the path corresponding to the alias. Trailing '/' and '\' characters + * will be trimmed. This can be * * - a directory or a file path (e.g. `/tmp`, `/tmp/main.txt`) * - a URL (e.g. `http://www.yiiframework.com`) * - a path alias (e.g. `@yii/base`). In this case, the path alias will be converted into the * actual path first by calling [[getAlias()]]. * - * @throws Exception if $path is an invalid alias + * @throws InvalidParamException the alias does not start with '@', or if $path is an invalid alias. * @see getAlias */ public static function setAlias($alias, $path) { - if ($path === null) { - unset(self::$aliases[$alias]); - } elseif (strncmp($path, '@', 1)) { - self::$aliases[$alias] = rtrim($path, '\\/'); - } else { - self::$aliases[$alias] = static::getAlias($path); + if (strncmp($alias, '@', 1)) { + throw new InvalidParamException('The alias must start with the "@" character.'); + } + $pos = strpos($alias, '/'); + $root = $pos === false ? $alias : substr($alias, 0, $pos); + if ($path !== null) { + $path = strncmp($path, '@', 1) ? rtrim($path, '\\/') : static::getAlias($path); + if (!isset(self::$aliases[$root])) { + self::$aliases[$root] = $path; + } elseif (is_string(self::$aliases[$root])) { + if ($pos === false) { + self::$aliases[$root] = $path; + } else { + self::$aliases[$root] = array( + $alias => $path, + $root => self::$aliases[$root], + ); + } + } else { + self::$aliases[$root][$alias] = $path; + krsort(self::$aliases[$root]); + } + } elseif (isset(self::$aliases[$root])) { + if (is_array(self::$aliases[$root])) { + unset(self::$aliases[$root][$alias]); + } elseif ($pos === false) { + unset(self::$aliases[$root]); + } } } @@ -258,14 +306,8 @@ class YiiBase } // search include_path - if (!isset($classFile) && self::$enableIncludePath) { - foreach (array_unique(explode(PATH_SEPARATOR, get_include_path())) as $basePath) { - $fullPath = $basePath . '/' . $path; - if (is_file($fullPath)) { - $classFile = $fullPath; - break; - } - } + if (!isset($classFile) && self::$enableIncludePath && ($fullPath = stream_resolve_include_path($path)) !== false) { + $classFile = $fullPath; } if (!isset($classFile)) { diff --git a/framework/caching/FileCache.php b/framework/caching/FileCache.php index e565cad..cc1a871 100644 --- a/framework/caching/FileCache.php +++ b/framework/caching/FileCache.php @@ -7,7 +7,7 @@ namespace yii\caching; -use yii\base\InvalidConfigException; +use Yii; /** * FileCache implements a cache component using files. @@ -51,7 +51,7 @@ class FileCache extends Cache public function init() { parent::init(); - $this->cachePath = \Yii::getAlias($this->cachePath); + $this->cachePath = Yii::getAlias($this->cachePath); if (!is_dir($this->cachePath)) { mkdir($this->cachePath, 0777, true); } diff --git a/tests/unit/framework/YiiBaseTest.php b/tests/unit/framework/YiiBaseTest.php index df12bf9..47474f2 100644 --- a/tests/unit/framework/YiiBaseTest.php +++ b/tests/unit/framework/YiiBaseTest.php @@ -1,6 +1,7 @@ aliases = Yii::$aliases; + } + + public function tearDown() + { + Yii::$aliases = $this->aliases; + } + public function testAlias() { + $this->assertEquals(YII_PATH, Yii::getAlias('@yii')); + + Yii::$aliases = array(); + $this->assertFalse(Yii::getAlias('@yii', false)); + + Yii::setAlias('@yii', '/yii/framework'); + $this->assertEquals('/yii/framework', Yii::getAlias('@yii')); + $this->assertEquals('/yii/framework/test/file', Yii::getAlias('@yii/test/file')); + Yii::setAlias('@yii/gii', '/yii/gii'); + $this->assertEquals('/yii/framework', Yii::getAlias('@yii')); + $this->assertEquals('/yii/framework/test/file', Yii::getAlias('@yii/test/file')); + $this->assertEquals('/yii/gii', Yii::getAlias('@yii/gii')); + $this->assertEquals('/yii/gii/file', Yii::getAlias('@yii/gii/file')); + + Yii::setAlias('@tii', '@yii/test'); + $this->assertEquals('/yii/framework/test', Yii::getAlias('@tii')); + Yii::setAlias('@yii', null); + $this->assertFalse(Yii::getAlias('@yii', false)); + $this->assertEquals('/yii/gii/file', Yii::getAlias('@yii/gii/file')); } public function testGetVersion() { - echo \Yii::getVersion(); + echo Yii::getVersion(); $this->assertTrue((boolean)preg_match('~\d+\.\d+(?:\.\d+)?(?:-\w+)?~', \Yii::getVersion())); } public function testPowered() { - $this->assertTrue(is_string(\Yii::powered())); + $this->assertTrue(is_string(Yii::powered())); } } From 3616278ada48fb316d7fbd1b57073518fad9e863 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Fri, 12 Apr 2013 07:08:01 -0400 Subject: [PATCH 054/104] typo. --- framework/base/Application.php | 2 +- framework/base/ErrorException.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/framework/base/Application.php b/framework/base/Application.php index e1c1d60..1dad257 100644 --- a/framework/base/Application.php +++ b/framework/base/Application.php @@ -165,7 +165,7 @@ class Application extends Module if (YII_ENABLE_ERROR_HANDLER) { $error = error_get_last(); - if (ErrorException::isFatalErorr($error)) { + if (ErrorException::isFatalError($error)) { unset($this->_memoryReserve); $exception = new ErrorException($error['message'], $error['type'], $error['type'], $error['file'], $error['line']); $this->logException($exception); diff --git a/framework/base/ErrorException.php b/framework/base/ErrorException.php index 93390e7..b41e9ed 100644 --- a/framework/base/ErrorException.php +++ b/framework/base/ErrorException.php @@ -79,7 +79,7 @@ class ErrorException extends Exception * @param array $error error got from error_get_last() * @return bool if error is one of fatal type */ - public static function isFatalErorr($error) + public static function isFatalError($error) { return isset($error['type']) && in_array($error['type'], array(E_ERROR, E_PARSE, E_CORE_ERROR, E_CORE_WARNING, E_COMPILE_ERROR, E_COMPILE_WARNING)); } From c1428a174add65820038463f80d2c8e0a9a8bb6a Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Fri, 12 Apr 2013 07:25:37 -0400 Subject: [PATCH 055/104] minor cleanup. --- framework/views/error.php | 2 +- framework/views/exception.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/framework/views/error.php b/framework/views/error.php index 893640a..548d04b 100644 --- a/framework/views/error.php +++ b/framework/views/error.php @@ -4,7 +4,7 @@ * @var \yii\base\ErrorHandler $context */ $context = $this->context; -$title = $context->htmlEncode($exception instanceof \yii\base\Exception || $exception instanceof \yii\base\ErrorException ? $exception->getName() : get_class($exception)); +$title = $context->htmlEncode($exception instanceof \yii\base\Exception ? $exception->getName() : get_class($exception)); ?> diff --git a/framework/views/exception.php b/framework/views/exception.php index db29302..f2aced0 100644 --- a/framework/views/exception.php +++ b/framework/views/exception.php @@ -4,7 +4,7 @@ * @var \yii\base\ErrorHandler $context */ $context = $this->context; -$title = $context->htmlEncode($exception instanceof \yii\base\Exception || $exception instanceof \yii\base\ErrorException ? $exception->getName().' ('.get_class($exception).')' : get_class($exception)); +$title = $context->htmlEncode($exception instanceof \yii\base\Exception ? $exception->getName().' ('.get_class($exception).')' : get_class($exception)); ?> From 428d912811654ef57992af226c0be9167a81527c Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Sat, 13 Apr 2013 13:05:54 -0400 Subject: [PATCH 056/104] script WIP --- framework/base/Application.php | 27 ++++++++++++++++++++++++++- framework/base/ViewContent.php | 17 +++++++++++++++-- 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/framework/base/Application.php b/framework/base/Application.php index 1dad257..1053210 100644 --- a/framework/base/Application.php +++ b/framework/base/Application.php @@ -87,7 +87,6 @@ class Application extends Module */ public $layout = 'main'; - private $_runtimePath; private $_ended = false; /** @@ -224,6 +223,8 @@ class Application extends Module return 0; } + private $_runtimePath; + /** * Returns the directory that stores runtime files. * @return string the directory that stores runtime files. Defaults to 'protected/runtime'. @@ -251,6 +252,30 @@ class Application extends Module } } + private $_vendorPath; + + /** + * Returns the directory that stores vendor files. + * @return string the directory that stores vendor files. Defaults to 'protected/vendor'. + */ + public function getVendorPath() + { + if ($this->_vendorPath !== null) { + $this->setVendorPath($this->getBasePath() . DIRECTORY_SEPARATOR . 'vendor'); + } + return $this->_vendorPath; + } + + /** + * Sets the directory that stores vendor files. + * @param string $path the directory that stores vendor files. + * @throws InvalidConfigException if the directory does not exist + */ + public function setVendorPath($path) + { + $this->_vendorPath = FileHelper::ensureDirectory($path); + } + /** * Returns the time zone used by this application. * This is a simple wrapper of PHP function date_default_timezone_get(). diff --git a/framework/base/ViewContent.php b/framework/base/ViewContent.php index 6a3b489..cf7684b 100644 --- a/framework/base/ViewContent.php +++ b/framework/base/ViewContent.php @@ -7,6 +7,8 @@ namespace yii\base; +use Yii; + /** * @author Qiang Xue * @since 2.0 @@ -43,7 +45,6 @@ class ViewContent extends Component * ~~~ */ public $bundles; - public $title; public $metaTags; public $linkTags; @@ -68,9 +69,21 @@ class ViewContent extends Component $this->jsFiles = null; } - public function registerBundle($name) + public function renderScripts($pos) { + } + public function registerBundle($name) + { + if (!isset($this->bundles[$name])) { + $am = Yii::$app->assets; + $bundle = $am->getBundle($name); + if ($bundle !== null) { + $this->bundles[$name] = $bundle; + } else { + throw new InvalidConfigException("Asset bundle does not exist: $name"); + } + } } public function getMetaTag($key) From 5cd7961a38d96708468d77a2ea6f4a894eef5241 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Sat, 13 Apr 2013 14:44:32 -0400 Subject: [PATCH 057/104] renamed clip to block. --- framework/base/View.php | 26 ++++++------- framework/base/ViewContent.php | 86 +++++------------------------------------- framework/widgets/Block.php | 49 ++++++++++++++++++++++++ framework/widgets/Clip.php | 51 ------------------------- 4 files changed, 72 insertions(+), 140 deletions(-) create mode 100644 framework/widgets/Block.php delete mode 100644 framework/widgets/Clip.php diff --git a/framework/base/View.php b/framework/base/View.php index 8b1f4ef..d1a3c5f 100644 --- a/framework/base/View.php +++ b/framework/base/View.php @@ -53,11 +53,12 @@ class View extends Component */ public $theme; /** - * @var array a list of named output clips. You can call [[beginClip()]] and [[endClip()]] + * @var array a list of named output blocks. The keys are the block names and the values + * are the corresponding block content. You can call [[beginBlock()]] and [[endBlock()]] * to capture small fragments of a view. They can be later accessed at somewhere else * through this property. */ - public $clips; + public $blocks; /** * @var Widget[] the widgets that are currently being rendered (not ended). This property * is maintained by [[beginWidget()]] and [[endWidget()]] methods. Do not modify it. @@ -350,26 +351,25 @@ class View extends Component } /** - * Begins recording a clip. - * This method is a shortcut to beginning [[yii\widgets\Clip]] - * @param string $id the clip ID. - * @param boolean $renderInPlace whether to render the clip content in place. - * Defaults to false, meaning the captured clip will not be displayed. - * @return \yii\widgets\Clip the Clip widget instance - * @see \yii\widgets\Clip + * Begins recording a block. + * This method is a shortcut to beginning [[yii\widgets\Block]] + * @param string $id the block ID. + * @param boolean $renderInPlace whether to render the block content in place. + * Defaults to false, meaning the captured block will not be displayed. + * @return \yii\widgets\Block the Block widget instance */ - public function beginClip($id, $renderInPlace = false) + public function beginBlock($id, $renderInPlace = false) { - return $this->beginWidget('yii\widgets\Clip', array( + return $this->beginWidget('yii\widgets\Block', array( 'id' => $id, 'renderInPlace' => $renderInPlace, )); } /** - * Ends recording a clip. + * Ends recording a block. */ - public function endClip() + public function endBlock() { $this->endWidget(); } diff --git a/framework/base/ViewContent.php b/framework/base/ViewContent.php index cf7684b..cea3c7c 100644 --- a/framework/base/ViewContent.php +++ b/framework/base/ViewContent.php @@ -49,9 +49,13 @@ class ViewContent extends Component public $metaTags; public $linkTags; public $css; - public $js; public $cssFiles; + public $js; public $jsFiles; + public $jsInHead; + public $jsFilesInHead; + public $jsInBody; + public $jsFilesInBody; public function populate($content) { @@ -64,86 +68,16 @@ class ViewContent extends Component $this->metaTags = null; $this->linkTags = null; $this->css = null; - $this->js = null; $this->cssFiles = null; + $this->js = null; $this->jsFiles = null; + $this->jsInHead = null; + $this->jsFilesInHead = null; + $this->jsInBody = null; + $this->jsFilesInBody = null; } public function renderScripts($pos) { } - - public function registerBundle($name) - { - if (!isset($this->bundles[$name])) { - $am = Yii::$app->assets; - $bundle = $am->getBundle($name); - if ($bundle !== null) { - $this->bundles[$name] = $bundle; - } else { - throw new InvalidConfigException("Asset bundle does not exist: $name"); - } - } - } - - public function getMetaTag($key) - { - return isset($this->metaTags[$key]) ? $this->metaTags[$key] : null; - } - - public function setMetaTag($key, $tag) - { - $this->metaTags[$key] = $tag; - } - - public function getLinkTag($key) - { - return isset($this->linkTags[$key]) ? $this->linkTags[$key] : null; - } - - public function setLinkTag($key, $tag) - { - $this->linkTags[$key] = $tag; - } - - public function getCss($key) - { - return isset($this->css[$key]) ? $this->css[$key]: null; - } - - public function setCss($key, $css) - { - $this->css[$key] = $css; - } - - public function getCssFile($key) - { - return isset($this->cssFiles[$key]) ? $this->cssFiles[$key]: null; - } - - public function setCssFile($key, $file) - { - $this->cssFiles[$key] = $file; - } - - public function getJs($key, $position = self::POS_END) - { - return isset($this->js[$position][$key]) ? $this->js[$position][$key] : null; - } - - public function setJs($key, $js, $position = self::POS_END) - { - $this->js[$position][$key] = $js; - } - - public function getJsFile($key, $position = self::POS_END) - { - return isset($this->jsFiles[$position][$key]) ? $this->jsFiles[$position][$key] : null; - } - - public function setJsFile($key, $file, $position = self::POS_END) - { - $this->jsFiles[$position][$key] = $file; - } - } \ No newline at end of file diff --git a/framework/widgets/Block.php b/framework/widgets/Block.php new file mode 100644 index 0000000..d6f7317 --- /dev/null +++ b/framework/widgets/Block.php @@ -0,0 +1,49 @@ + + * @since 2.0 + */ +class Block extends Widget +{ + /** + * @var string the ID of this block. + */ + public $id; + /** + * @var boolean whether to render the block content in place. Defaults to false, + * meaning the captured block content will not be displayed. + */ + public $renderInPlace = false; + + /** + * Starts recording a block. + */ + public function init() + { + ob_start(); + ob_implicit_flush(false); + } + + /** + * Ends recording a block. + * This method stops output buffering and saves the rendering result as a named block in the controller. + */ + public function run() + { + $block = ob_get_clean(); + if ($this->renderInPlace) { + echo $block; + } + $this->view->blocks[$this->id] = $block; + } +} \ No newline at end of file diff --git a/framework/widgets/Clip.php b/framework/widgets/Clip.php deleted file mode 100644 index f321209..0000000 --- a/framework/widgets/Clip.php +++ /dev/null @@ -1,51 +0,0 @@ - - * @since 2.0 - */ -class Clip extends Widget -{ - /** - * @var string the ID of this clip. - */ - public $id; - /** - * @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; - } - $this->view->clips[$this->id] = $clip; - } -} \ No newline at end of file From 9183e837532805f933a2a3addf83b0265ad16636 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Sat, 13 Apr 2013 14:51:50 -0400 Subject: [PATCH 058/104] restructured helper classes --- framework/helpers/ArrayHelper.php | 323 +--------- framework/helpers/ConsoleColor.php | 449 +------------- framework/helpers/FileHelper.php | 255 +------- framework/helpers/Html.php | 965 +---------------------------- framework/helpers/SecurityHelper.php | 245 +------- framework/helpers/StringHelper.php | 108 +--- framework/helpers/VarDumper.php | 108 +--- framework/helpers/base/ArrayHelper.php | 340 +++++++++++ framework/helpers/base/ConsoleColor.php | 470 ++++++++++++++ framework/helpers/base/FileHelper.php | 274 +++++++++ framework/helpers/base/Html.php | 981 ++++++++++++++++++++++++++++++ framework/helpers/base/SecurityHelper.php | 272 +++++++++ framework/helpers/base/StringHelper.php | 125 ++++ framework/helpers/base/VarDumper.php | 134 ++++ framework/helpers/base/mimeTypes.php | 187 ++++++ framework/helpers/mimeTypes.php | 187 ------ 16 files changed, 2790 insertions(+), 2633 deletions(-) create mode 100644 framework/helpers/base/ArrayHelper.php create mode 100644 framework/helpers/base/ConsoleColor.php create mode 100644 framework/helpers/base/FileHelper.php create mode 100644 framework/helpers/base/Html.php create mode 100644 framework/helpers/base/SecurityHelper.php create mode 100644 framework/helpers/base/StringHelper.php create mode 100644 framework/helpers/base/VarDumper.php create mode 100644 framework/helpers/base/mimeTypes.php delete mode 100644 framework/helpers/mimeTypes.php diff --git a/framework/helpers/ArrayHelper.php b/framework/helpers/ArrayHelper.php index 65fa962..3061717 100644 --- a/framework/helpers/ArrayHelper.php +++ b/framework/helpers/ArrayHelper.php @@ -7,9 +7,6 @@ namespace yii\helpers; -use Yii; -use yii\base\InvalidParamException; - /** * ArrayHelper provides additional array functionality you can use in your * application. @@ -17,324 +14,6 @@ use yii\base\InvalidParamException; * @author Qiang Xue * @since 2.0 */ -class ArrayHelper +class ArrayHelper extends base\ArrayHelper { - /** - * Merges two or more arrays into one recursively. - * If each array has an element with the same string key value, the latter - * will overwrite the former (different from array_merge_recursive). - * Recursive merging will be conducted if both arrays have an element of array - * type and are having the same key. - * For integer-keyed elements, the elements from the latter array will - * be appended to the former array. - * @param array $a array to be merged to - * @param array $b array to be merged from. You can specify additional - * arrays via third argument, fourth argument etc. - * @return array the merged array (the original arrays are not changed.) - */ - public static function merge($a, $b) - { - $args = func_get_args(); - $res = array_shift($args); - while ($args !== array()) { - $next = array_shift($args); - foreach ($next as $k => $v) { - if (is_integer($k)) { - isset($res[$k]) ? $res[] = $v : $res[$k] = $v; - } elseif (is_array($v) && isset($res[$k]) && is_array($res[$k])) { - $res[$k] = self::merge($res[$k], $v); - } else { - $res[$k] = $v; - } - } - } - return $res; - } - - /** - * Retrieves the value of an array element or object property with the given key or property name. - * If the key does not exist in the array, the default value will be returned instead. - * - * Below are some usage examples, - * - * ~~~ - * // working with array - * $username = \yii\helpers\ArrayHelper::getValue($_POST, 'username'); - * // working with object - * $username = \yii\helpers\ArrayHelper::getValue($user, 'username'); - * // working with anonymous function - * $fullName = \yii\helpers\ArrayHelper::getValue($user, function($user, $defaultValue) { - * return $user->firstName . ' ' . $user->lastName; - * }); - * ~~~ - * - * @param array|object $array array or object to extract value from - * @param string|\Closure $key key name of the array element, or property name of the object, - * or an anonymous function returning the value. The anonymous function signature should be: - * `function($array, $defaultValue)`. - * @param mixed $default the default value to be returned if the specified key does not exist - * @return mixed the value of the - */ - public static function getValue($array, $key, $default = null) - { - if ($key instanceof \Closure) { - return $key($array, $default); - } elseif (is_array($array)) { - return isset($array[$key]) || array_key_exists($key, $array) ? $array[$key] : $default; - } else { - return $array->$key; - } - } - - /** - * Indexes an array according to a specified key. - * The input array should be multidimensional or an array of objects. - * - * The key can be a key name of the sub-array, a property name of object, or an anonymous - * function which returns the key value given an array element. - * - * If a key value is null, the corresponding array element will be discarded and not put in the result. - * - * For example, - * - * ~~~ - * $array = array( - * array('id' => '123', 'data' => 'abc'), - * array('id' => '345', 'data' => 'def'), - * ); - * $result = ArrayHelper::index($array, 'id'); - * // the result is: - * // array( - * // '123' => array('id' => '123', 'data' => 'abc'), - * // '345' => array('id' => '345', 'data' => 'def'), - * // ) - * - * // using anonymous function - * $result = ArrayHelper::index($array, function(element) { - * return $element['id']; - * }); - * ~~~ - * - * @param array $array the array that needs to be indexed - * @param string|\Closure $key the column name or anonymous function whose result will be used to index the array - * @return array the indexed array - */ - public static function index($array, $key) - { - $result = array(); - foreach ($array as $element) { - $value = static::getValue($element, $key); - $result[$value] = $element; - } - return $result; - } - - /** - * Returns the values of a specified column in an array. - * The input array should be multidimensional or an array of objects. - * - * For example, - * - * ~~~ - * $array = array( - * array('id' => '123', 'data' => 'abc'), - * array('id' => '345', 'data' => 'def'), - * ); - * $result = ArrayHelper::getColumn($array, 'id'); - * // the result is: array( '123', '345') - * - * // using anonymous function - * $result = ArrayHelper::getColumn($array, function(element) { - * return $element['id']; - * }); - * ~~~ - * - * @param array $array - * @param string|\Closure $name - * @param boolean $keepKeys whether to maintain the array keys. If false, the resulting array - * will be re-indexed with integers. - * @return array the list of column values - */ - public static function getColumn($array, $name, $keepKeys = true) - { - $result = array(); - if ($keepKeys) { - foreach ($array as $k => $element) { - $result[$k] = static::getValue($element, $name); - } - } else { - foreach ($array as $element) { - $result[] = static::getValue($element, $name); - } - } - - return $result; - } - - /** - * Builds a map (key-value pairs) from a multidimensional array or an array of objects. - * The `$from` and `$to` parameters specify the key names or property names to set up the map. - * Optionally, one can further group the map according to a grouping field `$group`. - * - * For example, - * - * ~~~ - * $array = array( - * array('id' => '123', 'name' => 'aaa', 'class' => 'x'), - * array('id' => '124', 'name' => 'bbb', 'class' => 'x'), - * array('id' => '345', 'name' => 'ccc', 'class' => 'y'), - * ); - * - * $result = ArrayHelper::map($array, 'id', 'name'); - * // the result is: - * // array( - * // '123' => 'aaa', - * // '124' => 'bbb', - * // '345' => 'ccc', - * // ) - * - * $result = ArrayHelper::map($array, 'id', 'name', 'class'); - * // the result is: - * // array( - * // 'x' => array( - * // '123' => 'aaa', - * // '124' => 'bbb', - * // ), - * // 'y' => array( - * // '345' => 'ccc', - * // ), - * // ) - * ~~~ - * - * @param array $array - * @param string|\Closure $from - * @param string|\Closure $to - * @param string|\Closure $group - * @return array - */ - public static function map($array, $from, $to, $group = null) - { - $result = array(); - foreach ($array as $element) { - $key = static::getValue($element, $from); - $value = static::getValue($element, $to); - if ($group !== null) { - $result[static::getValue($element, $group)][$key] = $value; - } else { - $result[$key] = $value; - } - } - return $result; - } - - /** - * Sorts an array of objects or arrays (with the same structure) by one or several keys. - * @param array $array the array to be sorted. The array will be modified after calling this method. - * @param string|\Closure|array $key the key(s) to be sorted by. This refers to a key name of the sub-array - * elements, a property name of the objects, or an anonymous function returning the values for comparison - * purpose. The anonymous function signature should be: `function($item)`. - * To sort by multiple keys, provide an array of keys here. - * @param boolean|array $ascending whether to sort in ascending or descending order. When - * sorting by multiple keys with different ascending orders, use an array of ascending flags. - * @param integer|array $sortFlag the PHP sort flag. Valid values include: - * `SORT_REGULAR`, `SORT_NUMERIC`, `SORT_STRING`, and `SORT_STRING | SORT_FLAG_CASE`. The last - * value is for sorting strings in case-insensitive manner. Please refer to - * See [PHP manual](http://php.net/manual/en/function.sort.php) for more details. - * When sorting by multiple keys with different sort flags, use an array of sort flags. - * @throws InvalidParamException if the $ascending or $sortFlag parameters do not have - * correct number of elements as that of $key. - */ - public static function multisort(&$array, $key, $ascending = true, $sortFlag = SORT_REGULAR) - { - $keys = is_array($key) ? $key : array($key); - if (empty($keys) || empty($array)) { - return; - } - $n = count($keys); - if (is_scalar($ascending)) { - $ascending = array_fill(0, $n, $ascending); - } elseif (count($ascending) !== $n) { - throw new InvalidParamException('The length of $ascending parameter must be the same as that of $keys.'); - } - if (is_scalar($sortFlag)) { - $sortFlag = array_fill(0, $n, $sortFlag); - } elseif (count($sortFlag) !== $n) { - throw new InvalidParamException('The length of $ascending parameter must be the same as that of $keys.'); - } - $args = array(); - foreach ($keys as $i => $key) { - $flag = $sortFlag[$i]; - if ($flag == (SORT_STRING | SORT_FLAG_CASE)) { - $flag = SORT_STRING; - $column = array(); - foreach (static::getColumn($array, $key) as $k => $value) { - $column[$k] = strtolower($value); - } - $args[] = $column; - } else { - $args[] = static::getColumn($array, $key); - } - $args[] = $ascending[$i] ? SORT_ASC : SORT_DESC; - $args[] = $flag; - } - $args[] = &$array; - call_user_func_array('array_multisort', $args); - } - - /** - * Encodes special characters in an array of strings into HTML entities. - * Both the array keys and values will be encoded. - * If a value is an array, this method will also encode it recursively. - * @param array $data data to be encoded - * @param boolean $valuesOnly whether to encode array values only. If false, - * both the array keys and array values will be encoded. - * @param string $charset the charset that the data is using. If not set, - * [[\yii\base\Application::charset]] will be used. - * @return array the encoded data - * @see http://www.php.net/manual/en/function.htmlspecialchars.php - */ - public static function htmlEncode($data, $valuesOnly = true, $charset = null) - { - if ($charset === null) { - $charset = Yii::$app->charset; - } - $d = array(); - foreach ($data as $key => $value) { - if (!$valuesOnly && is_string($key)) { - $key = htmlspecialchars($key, ENT_QUOTES, $charset); - } - if (is_string($value)) { - $d[$key] = htmlspecialchars($value, ENT_QUOTES, $charset); - } elseif (is_array($value)) { - $d[$key] = static::htmlEncode($value, $charset); - } - } - return $d; - } - - /** - * Decodes HTML entities into the corresponding characters in an array of strings. - * Both the array keys and values will be decoded. - * If a value is an array, this method will also decode it recursively. - * @param array $data data to be decoded - * @param boolean $valuesOnly whether to decode array values only. If false, - * both the array keys and array values will be decoded. - * @return array the decoded data - * @see http://www.php.net/manual/en/function.htmlspecialchars-decode.php - */ - public static function htmlDecode($data, $valuesOnly = true) - { - $d = array(); - foreach ($data as $key => $value) { - if (!$valuesOnly && is_string($key)) { - $key = htmlspecialchars_decode($key, ENT_QUOTES); - } - if (is_string($value)) { - $d[$key] = htmlspecialchars_decode($value, ENT_QUOTES); - } elseif (is_array($value)) { - $d[$key] = static::htmlDecode($value); - } - } - return $d; - } } \ No newline at end of file diff --git a/framework/helpers/ConsoleColor.php b/framework/helpers/ConsoleColor.php index 429aeb1..794b9c8 100644 --- a/framework/helpers/ConsoleColor.php +++ b/framework/helpers/ConsoleColor.php @@ -18,453 +18,6 @@ namespace yii\helpers; * @author Carsten Brandt * @since 2.0 */ -class ConsoleColor +class ConsoleColor extends base\ConsoleColor { - const FG_BLACK = 30; - const FG_RED = 31; - const FG_GREEN = 32; - const FG_YELLOW = 33; - const FG_BLUE = 34; - const FG_PURPLE = 35; - const FG_CYAN = 36; - const FG_GREY = 37; - - const BG_BLACK = 40; - const BG_RED = 41; - const BG_GREEN = 42; - const BG_YELLOW = 43; - const BG_BLUE = 44; - const BG_PURPLE = 45; - const BG_CYAN = 46; - const BG_GREY = 47; - - const BOLD = 1; - const ITALIC = 3; - const UNDERLINE = 4; - const BLINK = 5; - const NEGATIVE = 7; - const CONCEALED = 8; - const CROSSED_OUT = 9; - const FRAMED = 51; - const ENCIRCLED = 52; - const OVERLINED = 53; - - /** - * Moves the terminal cursor up by sending ANSI control code CUU to the terminal. - * If the cursor is already at the edge of the screen, this has no effect. - * @param integer $rows number of rows the cursor should be moved up - */ - public static function moveCursorUp($rows=1) - { - echo "\033[" . (int) $rows . 'A'; - } - - /** - * Moves the terminal cursor down by sending ANSI control code CUD to the terminal. - * If the cursor is already at the edge of the screen, this has no effect. - * @param integer $rows number of rows the cursor should be moved down - */ - public static function moveCursorDown($rows=1) - { - echo "\033[" . (int) $rows . 'B'; - } - - /** - * Moves the terminal cursor forward by sending ANSI control code CUF to the terminal. - * If the cursor is already at the edge of the screen, this has no effect. - * @param integer $steps number of steps the cursor should be moved forward - */ - public static function moveCursorForward($steps=1) - { - echo "\033[" . (int) $steps . 'C'; - } - - /** - * Moves the terminal cursor backward by sending ANSI control code CUB to the terminal. - * If the cursor is already at the edge of the screen, this has no effect. - * @param integer $steps number of steps the cursor should be moved backward - */ - public static function moveCursorBackward($steps=1) - { - echo "\033[" . (int) $steps . 'D'; - } - - /** - * Moves the terminal cursor to the beginning of the next line by sending ANSI control code CNL to the terminal. - * @param integer $lines number of lines the cursor should be moved down - */ - public static function moveCursorNextLine($lines=1) - { - echo "\033[" . (int) $lines . 'E'; - } - - /** - * Moves the terminal cursor to the beginning of the previous line by sending ANSI control code CPL to the terminal. - * @param integer $lines number of lines the cursor should be moved up - */ - public static function moveCursorPrevLine($lines=1) - { - echo "\033[" . (int) $lines . 'F'; - } - - /** - * Moves the cursor to an absolute position given as column and row by sending ANSI control code CUP or CHA to the terminal. - * @param integer $column 1-based column number, 1 is the left edge of the screen. - * @param integer|null $row 1-based row number, 1 is the top edge of the screen. if not set, will move cursor only in current line. - */ - public static function moveCursorTo($column, $row=null) - { - if ($row === null) { - echo "\033[" . (int) $column . 'G'; - } else { - echo "\033[" . (int) $row . ';' . (int) $column . 'H'; - } - } - - /** - * Scrolls whole page up by sending ANSI control code SU to the terminal. - * New lines are added at the bottom. This is not supported by ANSI.SYS used in windows. - * @param int $lines number of lines to scroll up - */ - public static function scrollUp($lines=1) - { - echo "\033[".(int)$lines."S"; - } - - /** - * Scrolls whole page down by sending ANSI control code SD to the terminal. - * New lines are added at the top. This is not supported by ANSI.SYS used in windows. - * @param int $lines number of lines to scroll down - */ - public static function scrollDown($lines=1) - { - echo "\033[".(int)$lines."T"; - } - - /** - * Saves the current cursor position by sending ANSI control code SCP to the terminal. - * Position can then be restored with {@link restoreCursorPosition}. - */ - public static function saveCursorPosition() - { - echo "\033[s"; - } - - /** - * Restores the cursor position saved with {@link saveCursorPosition} by sending ANSI control code RCP to the terminal. - */ - public static function restoreCursorPosition() - { - echo "\033[u"; - } - - /** - * Hides the cursor by sending ANSI DECTCEM code ?25l to the terminal. - * Use {@link showCursor} to bring it back. - * Do not forget to show cursor when your application exits. Cursor might stay hidden in terminal after exit. - */ - public static function hideCursor() - { - echo "\033[?25l"; - } - - /** - * Will show a cursor again when it has been hidden by {@link hideCursor} by sending ANSI DECTCEM code ?25h to the terminal. - */ - public static function showCursor() - { - echo "\033[?25h"; - } - - /** - * Clears entire screen content by sending ANSI control code ED with argument 2 to the terminal. - * Cursor position will not be changed. - * **Note:** ANSI.SYS implementation used in windows will reset cursor position to upper left corner of the screen. - */ - public static function clearScreen() - { - echo "\033[2J"; - } - - /** - * Clears text from cursor to the beginning of the screen by sending ANSI control code ED with argument 1 to the terminal. - * Cursor position will not be changed. - */ - public static function clearScreenBeforeCursor() - { - echo "\033[1J"; - } - - /** - * Clears text from cursor to the end of the screen by sending ANSI control code ED with argument 0 to the terminal. - * Cursor position will not be changed. - */ - public static function clearScreenAfterCursor() - { - echo "\033[0J"; - } - - /** - * Clears the line, the cursor is currently on by sending ANSI control code EL with argument 2 to the terminal. - * Cursor position will not be changed. - */ - public static function clearLine() - { - echo "\033[2K"; - } - - /** - * Clears text from cursor position to the beginning of the line by sending ANSI control code EL with argument 1 to the terminal. - * Cursor position will not be changed. - */ - public static function clearLineBeforeCursor() - { - echo "\033[1K"; - } - - /** - * Clears text from cursor position to the end of the line by sending ANSI control code EL with argument 0 to the terminal. - * Cursor position will not be changed. - */ - public static function clearLineAfterCursor() - { - echo "\033[0K"; - } - - /** - * Will send ANSI format for following output - * - * You can pass any of the FG_*, BG_* and TEXT_* constants and also xterm256ColorBg - * TODO: documentation - */ - public static function ansiStyle() - { - echo "\033[" . implode(';', func_get_args()) . 'm'; - } - - /** - * Will return a string formatted with the given ANSI style - * - * See {@link ansiStyle} for possible arguments. - * @param string $string the string to be formatted - * @return string - */ - public static function ansiStyleString($string) - { - $args = func_get_args(); - array_shift($args); - $code = implode(';', $args); - return "\033[0m" . ($code !== '' ? "\033[" . $code . "m" : '') . $string."\033[0m"; - } - - //const COLOR_XTERM256 = 38;// http://en.wikipedia.org/wiki/Talk:ANSI_escape_code#xterm-256colors - public static function xterm256ColorFg($i) // TODO naming! - { - return '38;5;'.$i; - } - - public static function xterm256ColorBg($i) // TODO naming! - { - return '48;5;'.$i; - } - - /** - * Usage: list($w, $h) = ConsoleHelper::getScreenSize(); - * - * @return array - */ - public static function getScreenSize() - { - // TODO implement - return array(150,50); - } - - /** - * resets any ansi style set by previous method {@link ansiStyle} - * Any output after this is will have default text style. - */ - public static function reset() - { - echo "\033[0m"; - } - - /** - * Strips ANSI control codes from a string - * - * @param string $string String to strip - * @return string - */ - public static function strip($string) - { - return preg_replace('/\033\[[\d;]+m/', '', $string); // TODO currently only strips color - } - - // TODO refactor and review - public static function ansiToHtml($string) - { - $tags = 0; - return preg_replace_callback('/\033\[[\d;]+m/', function($ansi) use (&$tags) { - $styleA = array(); - foreach(explode(';', $ansi) as $controlCode) - { - switch($controlCode) - { - case static::FG_BLACK: $style = array('color' => '#000000'); break; - case static::FG_BLUE: $style = array('color' => '#000078'); break; - case static::FG_CYAN: $style = array('color' => '#007878'); break; - case static::FG_GREEN: $style = array('color' => '#007800'); break; - case static::FG_GREY: $style = array('color' => '#787878'); break; - case static::FG_PURPLE: $style = array('color' => '#780078'); break; - case static::FG_RED: $style = array('color' => '#780000'); break; - case static::FG_YELLOW: $style = array('color' => '#787800'); break; - case static::BG_BLACK: $style = array('background-color' => '#000000'); break; - case static::BG_BLUE: $style = array('background-color' => '#000078'); break; - case static::BG_CYAN: $style = array('background-color' => '#007878'); break; - case static::BG_GREEN: $style = array('background-color' => '#007800'); break; - case static::BG_GREY: $style = array('background-color' => '#787878'); break; - case static::BG_PURPLE: $style = array('background-color' => '#780078'); break; - case static::BG_RED: $style = array('background-color' => '#780000'); break; - case static::BG_YELLOW: $style = array('background-color' => '#787800'); break; - case static::BOLD: $style = array('font-weight' => 'bold'); break; - case static::ITALIC: $style = array('font-style' => 'italic'); break; - case static::UNDERLINE: $style = array('text-decoration' => array('underline')); break; - case static::OVERLINED: $style = array('text-decoration' => array('overline')); break; - case static::CROSSED_OUT:$style = array('text-decoration' => array('line-through')); break; - case static::BLINK: $style = array('text-decoration' => array('blink')); break; - case static::NEGATIVE: // ??? - case static::CONCEALED: - case static::ENCIRCLED: - case static::FRAMED: - // TODO allow resetting codes - break; - case 0: // ansi reset - $return = ''; - for($n=$tags; $tags>0; $tags--) { - $return .= ''; - } - return $return; - } - - $styleA = ArrayHelper::merge($styleA, $style); - } - $styleString[] = array(); - foreach($styleA as $name => $content) { - if ($name === 'text-decoration') { - $content = implode(' ', $content); - } - $styleString[] = $name.':'.$content; - } - $tags++; - return ' $ds, '\\' => $ds)), $ds); - } - - /** - * Returns the localized version of a specified file. - * - * The searching is based on the specified language code. In particular, - * a file with the same name will be looked for under the subdirectory - * whose name is same as the language code. For example, given the file "path/to/view.php" - * and language code "zh_cn", the localized file will be looked for as - * "path/to/zh_cn/view.php". If the file is not found, the original file - * will be returned. - * - * If the target and the source language codes are the same, - * the original file will be returned. - * - * For consistency, it is recommended that the language code is given - * in lower case and in the format of LanguageID_RegionID (e.g. "en_us"). - * - * @param string $file the original file - * @param string $language the target language that the file should be localized to. - * If not set, the value of [[\yii\base\Application::language]] will be used. - * @param string $sourceLanguage the language that the original file is in. - * If not set, the value of [[\yii\base\Application::sourceLanguage]] will be used. - * @return string the matching localized file, or the original file if the localized version is not found. - * If the target and the source language codes are the same, the original file will be returned. - */ - public static function localize($file, $language = null, $sourceLanguage = null) - { - if ($language === null) { - $language = \Yii::$app->language; - } - if ($sourceLanguage === null) { - $sourceLanguage = \Yii::$app->sourceLanguage; - } - if ($language === $sourceLanguage) { - return $file; - } - $desiredFile = dirname($file) . DIRECTORY_SEPARATOR . $sourceLanguage . DIRECTORY_SEPARATOR . basename($file); - return is_file($desiredFile) ? $desiredFile : $file; - } - - /** - * Determines the MIME type of the specified file. - * This method will first try to determine the MIME type based on - * [finfo_open](http://php.net/manual/en/function.finfo-open.php). If this doesn't work, it will - * fall back to [[getMimeTypeByExtension()]]. - * @param string $file the file name. - * @param string $magicFile name of the optional magic database file, usually something like `/path/to/magic.mime`. - * This will be passed as the second parameter to [finfo_open](http://php.net/manual/en/function.finfo-open.php). - * @param boolean $checkExtension whether to use the file extension to determine the MIME type in case - * `finfo_open()` cannot determine it. - * @return string the MIME type (e.g. `text/plain`). Null is returned if the MIME type cannot be determined. - */ - public static function getMimeType($file, $magicFile = null, $checkExtension = true) - { - if (function_exists('finfo_open')) { - $info = finfo_open(FILEINFO_MIME_TYPE, $magicFile); - if ($info && ($result = finfo_file($info, $file)) !== false) { - return $result; - } - } - - return $checkExtension ? self::getMimeTypeByExtension($file) : null; - } - - /** - * Determines the MIME type based on the extension name of the specified file. - * This method will use a local map between extension names and MIME types. - * @param string $file the file name. - * @param string $magicFile the path of the file that contains all available MIME type information. - * If this is not set, the default file aliased by `@yii/util/mimeTypes.php` will be used. - * @return string the MIME type. Null is returned if the MIME type cannot be determined. - */ - public static function getMimeTypeByExtension($file, $magicFile = null) - { - if ($magicFile === null) { - $magicFile = \Yii::getAlias('@yii/util/mimeTypes.php'); - } - $mimeTypes = require($magicFile); - if (($ext = pathinfo($file, PATHINFO_EXTENSION)) !== '') { - $ext = strtolower($ext); - if (isset($mimeTypes[$ext])) { - return $mimeTypes[$ext]; - } - } - return null; - } - - /** - * Copies a list of files from one place to another. - * @param array $fileList the list of files to be copied (name=>spec). - * The array keys are names displayed during the copy process, and array values are specifications - * for files to be copied. Each array value must be an array of the following structure: - *
      - *
    • source: required, the full path of the file/directory to be copied from
    • - *
    • target: required, the full path of the file/directory to be copied to
    • - *
    • callback: optional, the callback to be invoked when copying a file. The callback function - * should be declared as follows: - *
      -	 *   function foo($source,$params)
      -	 *   
      - * where $source parameter is the source file path, and the content returned - * by the function will be saved into the target file.
    • - *
    • params: optional, the parameters to be passed to the callback
    • - *
    - * @see buildFileList - */ - public static function copyFiles($fileList) - { - $overwriteAll = false; - foreach($fileList as $name=>$file) { - $source = strtr($file['source'], '/\\', DIRECTORY_SEPARATOR); - $target = strtr($file['target'], '/\\', DIRECTORY_SEPARATOR); - $callback = isset($file['callback']) ? $file['callback'] : null; - $params = isset($file['params']) ? $file['params'] : null; - - if(is_dir($source)) { - try { - self::ensureDirectory($target); - } - catch (Exception $e) { - mkdir($target, true, 0777); - } - continue; - } - - if($callback !== null) { - $content = call_user_func($callback, $source, $params); - } - else { - $content = file_get_contents($source); - } - if(is_file($target)) { - if($content === file_get_contents($target)) { - echo " unchanged $name\n"; - continue; - } - if($overwriteAll) { - echo " overwrite $name\n"; - } - else { - echo " exist $name\n"; - echo " ...overwrite? [Yes|No|All|Quit] "; - $answer = trim(fgets(STDIN)); - if(!strncasecmp($answer, 'q', 1)) { - return; - } - elseif(!strncasecmp($answer, 'y', 1)) { - echo " overwrite $name\n"; - } - elseif(!strncasecmp($answer, 'a', 1)) { - echo " overwrite $name\n"; - $overwriteAll = true; - } - else { - echo " skip $name\n"; - continue; - } - } - } - else { - try { - self::ensureDirectory(dirname($target)); - } - catch (Exception $e) { - mkdir(dirname($target), true, 0777); - } - echo " generate $name\n"; - } - file_put_contents($target, $content); - } - } - - /** - * Builds the file list of a directory. - * This method traverses through the specified directory and builds - * a list of files and subdirectories that the directory contains. - * The result of this function can be passed to {@link copyFiles}. - * @param string $sourceDir the source directory - * @param string $targetDir the target directory - * @param string $baseDir base directory - * @param array $ignoreFiles list of the names of files that should - * be ignored in list building process. Argument available since 1.1.11. - * @param array $renameMap hash array of file names that should be - * renamed. Example value: array('1.old.txt'=>'2.new.txt'). - * @return array the file list (see {@link copyFiles}) - */ - public static function buildFileList($sourceDir, $targetDir, $baseDir='', $ignoreFiles=array(), $renameMap=array()) - { - $list = array(); - $handle = opendir($sourceDir); - while(($file = readdir($handle)) !== false) { - if(in_array($file, array('.', '..', '.svn', '.gitignore')) || in_array($file, $ignoreFiles)) { - continue; - } - $sourcePath = $sourceDir.DIRECTORY_SEPARATOR.$file; - $targetPath = $targetDir.DIRECTORY_SEPARATOR.strtr($file, $renameMap); - $name = $baseDir === '' ? $file : $baseDir.'/'.$file; - $list[$name] = array( - 'source' => $sourcePath, - 'target' => $targetPath, - ); - if(is_dir($sourcePath)) { - $list = array_merge($list, self::buildFileList($sourcePath, $targetPath, $name, $ignoreFiles, $renameMap)); - } - } - closedir($handle); - return $list; - } } \ No newline at end of file diff --git a/framework/helpers/Html.php b/framework/helpers/Html.php index b2ca576..b3a0743 100644 --- a/framework/helpers/Html.php +++ b/framework/helpers/Html.php @@ -7,975 +7,12 @@ namespace yii\helpers; -use Yii; -use yii\base\InvalidParamException; - /** * Html provides a set of static methods for generating commonly used HTML tags. * * @author Qiang Xue * @since 2.0 */ -class Html +class Html extends base\Html { - /** - * @var boolean whether to close void (empty) elements. Defaults to true. - * @see voidElements - */ - public static $closeVoidElements = true; - /** - * @var array list of void elements (element name => 1) - * @see closeVoidElements - * @see http://www.w3.org/TR/html-markup/syntax.html#void-element - */ - public static $voidElements = array( - 'area' => 1, - 'base' => 1, - 'br' => 1, - 'col' => 1, - 'command' => 1, - 'embed' => 1, - 'hr' => 1, - 'img' => 1, - 'input' => 1, - 'keygen' => 1, - 'link' => 1, - 'meta' => 1, - 'param' => 1, - 'source' => 1, - 'track' => 1, - 'wbr' => 1, - ); - /** - * @var boolean whether to show the values of boolean attributes in element tags. - * If false, only the attribute names will be generated. - * @see booleanAttributes - */ - public static $showBooleanAttributeValues = true; - /** - * @var array list of boolean attributes. The presence of a boolean attribute on - * an element represents the true value, and the absence of the attribute represents the false value. - * @see showBooleanAttributeValues - * @see http://www.w3.org/TR/html5/infrastructure.html#boolean-attributes - */ - public static $booleanAttributes = array( - 'async' => 1, - 'autofocus' => 1, - 'autoplay' => 1, - 'checked' => 1, - 'controls' => 1, - 'declare' => 1, - 'default' => 1, - 'defer' => 1, - 'disabled' => 1, - 'formnovalidate' => 1, - 'hidden' => 1, - 'ismap' => 1, - 'loop' => 1, - 'multiple' => 1, - 'muted' => 1, - 'nohref' => 1, - 'noresize' => 1, - 'novalidate' => 1, - 'open' => 1, - 'readonly' => 1, - 'required' => 1, - 'reversed' => 1, - 'scoped' => 1, - 'seamless' => 1, - 'selected' => 1, - 'typemustmatch' => 1, - ); - /** - * @var array the preferred order of attributes in a tag. This mainly affects the order of the attributes - * that are rendered by [[renderAttributes()]]. - */ - public static $attributeOrder = array( - 'type', - 'id', - 'class', - 'name', - 'value', - - 'href', - 'src', - 'action', - 'method', - - 'selected', - 'checked', - 'readonly', - 'disabled', - 'multiple', - - 'size', - 'maxlength', - 'width', - 'height', - 'rows', - 'cols', - - 'alt', - 'title', - 'rel', - 'media', - ); - - /** - * Encodes special characters into HTML entities. - * The [[yii\base\Application::charset|application charset]] will be used for encoding. - * @param string $content the content to be encoded - * @return string the encoded content - * @see decode - * @see http://www.php.net/manual/en/function.htmlspecialchars.php - */ - public static function encode($content) - { - return htmlspecialchars($content, ENT_QUOTES, Yii::$app->charset); - } - - /** - * Decodes special HTML entities back to the corresponding characters. - * This is the opposite of [[encode()]]. - * @param string $content the content to be decoded - * @return string the decoded content - * @see encode - * @see http://www.php.net/manual/en/function.htmlspecialchars-decode.php - */ - public static function decode($content) - { - return htmlspecialchars_decode($content, ENT_QUOTES); - } - - /** - * Generates a complete HTML tag. - * @param string $name the tag name - * @param string $content the content to be enclosed between the start and end tags. It will not be HTML-encoded. - * If this is coming from end users, you should consider [[encode()]] it to prevent XSS attacks. - * @param array $options the tag options in terms of name-value pairs. These will be rendered as - * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. - * If a value is null, the corresponding attribute will not be rendered. - * @return string the generated HTML tag - * @see beginTag - * @see endTag - */ - public static function tag($name, $content = '', $options = array()) - { - $html = '<' . $name . static::renderTagAttributes($options); - if (isset(static::$voidElements[strtolower($name)])) { - return $html . (static::$closeVoidElements ? ' />' : '>'); - } else { - return $html . ">$content"; - } - } - - /** - * Generates a start tag. - * @param string $name the tag name - * @param array $options the tag options in terms of name-value pairs. These will be rendered as - * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. - * If a value is null, the corresponding attribute will not be rendered. - * @return string the generated start tag - * @see endTag - * @see tag - */ - public static function beginTag($name, $options = array()) - { - return '<' . $name . static::renderTagAttributes($options) . '>'; - } - - /** - * Generates an end tag. - * @param string $name the tag name - * @return string the generated end tag - * @see beginTag - * @see tag - */ - public static function endTag($name) - { - return ""; - } - - /** - * Encloses the given content within a CDATA tag. - * @param string $content the content to be enclosed within the CDATA tag - * @return string the CDATA tag with the enclosed content. - */ - public static function cdata($content) - { - return ''; - } - - /** - * Generates a style tag. - * @param string $content the style content - * @param array $options the tag options in terms of name-value pairs. These will be rendered as - * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. - * If a value is null, the corresponding attribute will not be rendered. - * If the options does not contain "type", a "type" attribute with value "text/css" will be used. - * @return string the generated style tag - */ - public static function style($content, $options = array()) - { - if (!isset($options['type'])) { - $options['type'] = 'text/css'; - } - return static::tag('style', "/**/", $options); - } - - /** - * Generates a script tag. - * @param string $content the script content - * @param array $options the tag options in terms of name-value pairs. These will be rendered as - * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. - * If a value is null, the corresponding attribute will not be rendered. - * If the options does not contain "type", a "type" attribute with value "text/javascript" will be rendered. - * @return string the generated script tag - */ - public static function script($content, $options = array()) - { - if (!isset($options['type'])) { - $options['type'] = 'text/javascript'; - } - return static::tag('script', "/**/", $options); - } - - /** - * Generates a link tag that refers to an external CSS file. - * @param array|string $url the URL of the external CSS file. This parameter will be processed by [[url()]]. - * @param array $options the tag options in terms of name-value pairs. These will be rendered as - * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. - * If a value is null, the corresponding attribute will not be rendered. - * @return string the generated link tag - * @see url - */ - public static function cssFile($url, $options = array()) - { - $options['rel'] = 'stylesheet'; - $options['type'] = 'text/css'; - $options['href'] = static::url($url); - return static::tag('link', '', $options); - } - - /** - * Generates a script tag that refers to an external JavaScript file. - * @param string $url the URL of the external JavaScript file. This parameter will be processed by [[url()]]. - * @param array $options the tag options in terms of name-value pairs. These will be rendered as - * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. - * If a value is null, the corresponding attribute will not be rendered. - * @return string the generated script tag - * @see url - */ - public static function jsFile($url, $options = array()) - { - $options['type'] = 'text/javascript'; - $options['src'] = static::url($url); - return static::tag('script', '', $options); - } - - /** - * Generates a form start tag. - * @param array|string $action the form action URL. This parameter will be processed by [[url()]]. - * @param string $method the form submission method, either "post" or "get" (case-insensitive) - * @param array $options the tag options in terms of name-value pairs. These will be rendered as - * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. - * If a value is null, the corresponding attribute will not be rendered. - * @return string the generated form start tag. - * @see endForm - */ - public static function beginForm($action = '', $method = 'post', $options = array()) - { - $action = static::url($action); - - // query parameters in the action are ignored for GET method - // we use hidden fields to add them back - $hiddens = array(); - if (!strcasecmp($method, 'get') && ($pos = strpos($action, '?')) !== false) { - foreach (explode('&', substr($action, $pos + 1)) as $pair) { - if (($pos1 = strpos($pair, '=')) !== false) { - $hiddens[] = static::hiddenInput(urldecode(substr($pair, 0, $pos1)), urldecode(substr($pair, $pos1 + 1))); - } else { - $hiddens[] = static::hiddenInput(urldecode($pair), ''); - } - } - $action = substr($action, 0, $pos); - } - - $options['action'] = $action; - $options['method'] = $method; - $form = static::beginTag('form', $options); - if ($hiddens !== array()) { - $form .= "\n" . implode("\n", $hiddens); - } - - return $form; - } - - /** - * Generates a form end tag. - * @return string the generated tag - * @see beginForm - */ - public static function endForm() - { - return ''; - } - - /** - * Generates a hyperlink tag. - * @param string $text link body. It will NOT be HTML-encoded. Therefore you can pass in HTML code - * such as an image tag. If this is is coming from end users, you should consider [[encode()]] - * it to prevent XSS attacks. - * @param array|string|null $url the URL for the hyperlink tag. This parameter will be processed by [[url()]] - * and will be used for the "href" attribute of the tag. If this parameter is null, the "href" attribute - * will not be generated. - * @param array $options the tag options in terms of name-value pairs. These will be rendered as - * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. - * If a value is null, the corresponding attribute will not be rendered. - * @return string the generated hyperlink - * @see url - */ - public static function a($text, $url = null, $options = array()) - { - if ($url !== null) { - $options['href'] = static::url($url); - } - return static::tag('a', $text, $options); - } - - /** - * Generates a mailto hyperlink. - * @param string $text link body. It will NOT be HTML-encoded. Therefore you can pass in HTML code - * such as an image tag. If this is is coming from end users, you should consider [[encode()]] - * it to prevent XSS attacks. - * @param string $email email address. If this is null, the first parameter (link body) will be treated - * as the email address and used. - * @param array $options the tag options in terms of name-value pairs. These will be rendered as - * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. - * If a value is null, the corresponding attribute will not be rendered. - * @return string the generated mailto link - */ - public static function mailto($text, $email = null, $options = array()) - { - return static::a($text, 'mailto:' . ($email === null ? $text : $email), $options); - } - - /** - * Generates an image tag. - * @param string $src the image URL. This parameter will be processed by [[url()]]. - * @param array $options the tag options in terms of name-value pairs. These will be rendered as - * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. - * If a value is null, the corresponding attribute will not be rendered. - * @return string the generated image tag - */ - public static function img($src, $options = array()) - { - $options['src'] = static::url($src); - if (!isset($options['alt'])) { - $options['alt'] = ''; - } - return static::tag('img', null, $options); - } - - /** - * Generates a label tag. - * @param string $content label text. It will NOT be HTML-encoded. Therefore you can pass in HTML code - * such as an image tag. If this is is coming from end users, you should consider [[encode()]] - * it to prevent XSS attacks. - * @param string $for the ID of the HTML element that this label is associated with. - * If this is null, the "for" attribute will not be generated. - * @param array $options the tag options in terms of name-value pairs. These will be rendered as - * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. - * If a value is null, the corresponding attribute will not be rendered. - * @return string the generated label tag - */ - public static function label($content, $for = null, $options = array()) - { - $options['for'] = $for; - return static::tag('label', $content, $options); - } - - /** - * Generates a button tag. - * @param string $name the name attribute. If it is null, the name attribute will not be generated. - * @param string $value the value attribute. If it is null, the value attribute will not be generated. - * @param string $content the content enclosed within the button tag. It will NOT be HTML-encoded. - * Therefore you can pass in HTML code such as an image tag. If this is is coming from end users, - * you should consider [[encode()]] it to prevent XSS attacks. - * @param array $options the tag options in terms of name-value pairs. These will be rendered as - * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. - * If a value is null, the corresponding attribute will not be rendered. - * If the options does not contain "type", a "type" attribute with value "button" will be rendered. - * @return string the generated button tag - */ - public static function button($name = null, $value = null, $content = 'Button', $options = array()) - { - $options['name'] = $name; - $options['value'] = $value; - if (!isset($options['type'])) { - $options['type'] = 'button'; - } - return static::tag('button', $content, $options); - } - - /** - * Generates a submit button tag. - * @param string $name the name attribute. If it is null, the name attribute will not be generated. - * @param string $value the value attribute. If it is null, the value attribute will not be generated. - * @param string $content the content enclosed within the button tag. It will NOT be HTML-encoded. - * Therefore you can pass in HTML code such as an image tag. If this is is coming from end users, - * you should consider [[encode()]] it to prevent XSS attacks. - * @param array $options the tag options in terms of name-value pairs. These will be rendered as - * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. - * If a value is null, the corresponding attribute will not be rendered. - * @return string the generated submit button tag - */ - public static function submitButton($name = null, $value = null, $content = 'Submit', $options = array()) - { - $options['type'] = 'submit'; - return static::button($name, $value, $content, $options); - } - - /** - * Generates a reset button tag. - * @param string $name the name attribute. If it is null, the name attribute will not be generated. - * @param string $value the value attribute. If it is null, the value attribute will not be generated. - * @param string $content the content enclosed within the button tag. It will NOT be HTML-encoded. - * Therefore you can pass in HTML code such as an image tag. If this is is coming from end users, - * you should consider [[encode()]] it to prevent XSS attacks. - * @param array $options the tag options in terms of name-value pairs. These will be rendered as - * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. - * If a value is null, the corresponding attribute will not be rendered. - * @return string the generated reset button tag - */ - public static function resetButton($name = null, $value = null, $content = 'Reset', $options = array()) - { - $options['type'] = 'reset'; - return static::button($name, $value, $content, $options); - } - - /** - * Generates an input type of the given type. - * @param string $type the type attribute. - * @param string $name the name attribute. If it is null, the name attribute will not be generated. - * @param string $value the value attribute. If it is null, the value attribute will not be generated. - * @param array $options the tag options in terms of name-value pairs. These will be rendered as - * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. - * If a value is null, the corresponding attribute will not be rendered. - * @return string the generated input tag - */ - public static function input($type, $name = null, $value = null, $options = array()) - { - $options['type'] = $type; - $options['name'] = $name; - $options['value'] = $value; - return static::tag('input', null, $options); - } - - /** - * Generates an input button. - * @param string $name the name attribute. - * @param string $value the value attribute. If it is null, the value attribute will not be generated. - * @param array $options the tag options in terms of name-value pairs. These will be rendered as - * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. - * If a value is null, the corresponding attribute will not be rendered. - * @return string the generated button tag - */ - public static function buttonInput($name, $value = 'Button', $options = array()) - { - return static::input('button', $name, $value, $options); - } - - /** - * Generates a submit input button. - * @param string $name the name attribute. If it is null, the name attribute will not be generated. - * @param string $value the value attribute. If it is null, the value attribute will not be generated. - * @param array $options the tag options in terms of name-value pairs. These will be rendered as - * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. - * If a value is null, the corresponding attribute will not be rendered. - * @return string the generated button tag - */ - public static function submitInput($name = null, $value = 'Submit', $options = array()) - { - return static::input('submit', $name, $value, $options); - } - - /** - * Generates a reset input button. - * @param string $name the name attribute. If it is null, the name attribute will not be generated. - * @param string $value the value attribute. If it is null, the value attribute will not be generated. - * @param array $options the attributes of the button tag. The values will be HTML-encoded using [[encode()]]. - * Attributes whose value is null will be ignored and not put in the tag returned. - * @return string the generated button tag - */ - public static function resetInput($name = null, $value = 'Reset', $options = array()) - { - return static::input('reset', $name, $value, $options); - } - - /** - * Generates a text input field. - * @param string $name the name attribute. - * @param string $value the value attribute. If it is null, the value attribute will not be generated. - * @param array $options the tag options in terms of name-value pairs. These will be rendered as - * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. - * If a value is null, the corresponding attribute will not be rendered. - * @return string the generated button tag - */ - public static function textInput($name, $value = null, $options = array()) - { - return static::input('text', $name, $value, $options); - } - - /** - * Generates a hidden input field. - * @param string $name the name attribute. - * @param string $value the value attribute. If it is null, the value attribute will not be generated. - * @param array $options the tag options in terms of name-value pairs. These will be rendered as - * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. - * If a value is null, the corresponding attribute will not be rendered. - * @return string the generated button tag - */ - public static function hiddenInput($name, $value = null, $options = array()) - { - return static::input('hidden', $name, $value, $options); - } - - /** - * Generates a password input field. - * @param string $name the name attribute. - * @param string $value the value attribute. If it is null, the value attribute will not be generated. - * @param array $options the tag options in terms of name-value pairs. These will be rendered as - * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. - * If a value is null, the corresponding attribute will not be rendered. - * @return string the generated button tag - */ - public static function passwordInput($name, $value = null, $options = array()) - { - return static::input('password', $name, $value, $options); - } - - /** - * Generates a file input field. - * To use a file input field, you should set the enclosing form's "enctype" attribute to - * be "multipart/form-data". After the form is submitted, the uploaded file information - * can be obtained via $_FILES[$name] (see PHP documentation). - * @param string $name the name attribute. - * @param string $value the value attribute. If it is null, the value attribute will not be generated. - * @param array $options the tag options in terms of name-value pairs. These will be rendered as - * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. - * If a value is null, the corresponding attribute will not be rendered. - * @return string the generated button tag - */ - public static function fileInput($name, $value = null, $options = array()) - { - return static::input('file', $name, $value, $options); - } - - /** - * Generates a text area input. - * @param string $name the input name - * @param string $value the input value. Note that it will be encoded using [[encode()]]. - * @param array $options the tag options in terms of name-value pairs. These will be rendered as - * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. - * If a value is null, the corresponding attribute will not be rendered. - * @return string the generated text area tag - */ - public static function textarea($name, $value = '', $options = array()) - { - $options['name'] = $name; - return static::tag('textarea', static::encode($value), $options); - } - - /** - * Generates a radio button input. - * @param string $name the name attribute. - * @param boolean $checked whether the radio button should be checked. - * @param string $value the value attribute. If it is null, the value attribute will not be rendered. - * @param array $options the tag options in terms of name-value pairs. The following options are supported: - * - * - uncheck: string, the value associated with the uncheck state of the radio button. When this attribute - * is present, a hidden input will be generated so that if the radio button is not checked and is submitted, - * the value of this attribute will still be submitted to the server via the hidden input. - * - * The rest of the options will be rendered as the attributes of the resulting tag. The values will - * be HTML-encoded using [[encode()]]. If a value is null, the corresponding attribute will not be rendered. - * - * @return string the generated radio button tag - */ - public static function radio($name, $checked = false, $value = '1', $options = array()) - { - $options['checked'] = $checked; - $options['value'] = $value; - if (isset($options['uncheck'])) { - // add a hidden field so that if the radio button is not selected, it still submits a value - $hidden = static::hiddenInput($name, $options['uncheck']); - unset($options['uncheck']); - } else { - $hidden = ''; - } - return $hidden . static::input('radio', $name, $value, $options); - } - - /** - * Generates a checkbox input. - * @param string $name the name attribute. - * @param boolean $checked whether the checkbox should be checked. - * @param string $value the value attribute. If it is null, the value attribute will not be rendered. - * @param array $options the tag options in terms of name-value pairs. The following options are supported: - * - * - uncheck: string, the value associated with the uncheck state of the checkbox. When this attribute - * is present, a hidden input will be generated so that if the checkbox is not checked and is submitted, - * the value of this attribute will still be submitted to the server via the hidden input. - * - * The rest of the options will be rendered as the attributes of the resulting tag. The values will - * be HTML-encoded using [[encode()]]. If a value is null, the corresponding attribute will not be rendered. - * - * @return string the generated checkbox tag - */ - public static function checkbox($name, $checked = false, $value = '1', $options = array()) - { - $options['checked'] = $checked; - $options['value'] = $value; - if (isset($options['uncheck'])) { - // add a hidden field so that if the checkbox is not selected, it still submits a value - $hidden = static::hiddenInput($name, $options['uncheck']); - unset($options['uncheck']); - } else { - $hidden = ''; - } - return $hidden . static::input('checkbox', $name, $value, $options); - } - - /** - * Generates a drop-down list. - * @param string $name the input name - * @param string $selection the selected value - * @param array $items the option data items. The array keys are option values, and the array values - * are the corresponding option labels. The array can also be nested (i.e. some array values are arrays too). - * For each sub-array, an option group will be generated whose label is the key associated with the sub-array. - * If you have a list of data models, you may convert them into the format described above using - * [[\yii\helpers\ArrayHelper::map()]]. - * - * Note, the values and labels will be automatically HTML-encoded by this method, and the blank spaces in - * the labels will also be HTML-encoded. - * @param array $options the tag options in terms of name-value pairs. The following options are supported: - * - * - prompt: string, a prompt text to be displayed as the first option; - * - options: array, the attributes for the select option tags. The array keys must be valid option values, - * and the array values are the extra attributes for the corresponding option tags. For example, - * - * ~~~ - * array( - * 'value1' => array('disabled' => true), - * 'value2' => array('label' => 'value 2'), - * ); - * ~~~ - * - * - groups: array, the attributes for the optgroup tags. The structure of this is similar to that of 'options', - * except that the array keys represent the optgroup labels specified in $items. - * - * The rest of the options will be rendered as the attributes of the resulting tag. The values will - * be HTML-encoded using [[encode()]]. If a value is null, the corresponding attribute will not be rendered. - * - * @return string the generated drop-down list tag - */ - public static function dropDownList($name, $selection = null, $items = array(), $options = array()) - { - $options['name'] = $name; - $selectOptions = static::renderSelectOptions($selection, $items, $options); - return static::tag('select', "\n" . $selectOptions . "\n", $options); - } - - /** - * Generates a list box. - * @param string $name the input name - * @param string|array $selection the selected value(s) - * @param array $items the option data items. The array keys are option values, and the array values - * are the corresponding option labels. The array can also be nested (i.e. some array values are arrays too). - * For each sub-array, an option group will be generated whose label is the key associated with the sub-array. - * If you have a list of data models, you may convert them into the format described above using - * [[\yii\helpers\ArrayHelper::map()]]. - * - * Note, the values and labels will be automatically HTML-encoded by this method, and the blank spaces in - * the labels will also be HTML-encoded. - * @param array $options the tag options in terms of name-value pairs. The following options are supported: - * - * - prompt: string, a prompt text to be displayed as the first option; - * - options: array, the attributes for the select option tags. The array keys must be valid option values, - * and the array values are the extra attributes for the corresponding option tags. For example, - * - * ~~~ - * array( - * 'value1' => array('disabled' => true), - * 'value2' => array('label' => 'value 2'), - * ); - * ~~~ - * - * - groups: array, the attributes for the optgroup tags. The structure of this is similar to that of 'options', - * except that the array keys represent the optgroup labels specified in $items. - * - unselect: string, the value that will be submitted when no option is selected. - * When this attribute is set, a hidden field will be generated so that if no option is selected in multiple - * mode, we can still obtain the posted unselect value. - * - * The rest of the options will be rendered as the attributes of the resulting tag. The values will - * be HTML-encoded using [[encode()]]. If a value is null, the corresponding attribute will not be rendered. - * - * @return string the generated list box tag - */ - public static function listBox($name, $selection = null, $items = array(), $options = array()) - { - if (!isset($options['size'])) { - $options['size'] = 4; - } - if (isset($options['multiple']) && $options['multiple'] && substr($name, -2) !== '[]') { - $name .= '[]'; - } - $options['name'] = $name; - if (isset($options['unselect'])) { - // add a hidden field so that if the list box has no option being selected, it still submits a value - if (substr($name, -2) === '[]') { - $name = substr($name, 0, -2); - } - $hidden = static::hiddenInput($name, $options['unselect']); - unset($options['unselect']); - } else { - $hidden = ''; - } - $selectOptions = static::renderSelectOptions($selection, $items, $options); - return $hidden . static::tag('select', "\n" . $selectOptions . "\n", $options); - } - - /** - * Generates a list of checkboxes. - * A checkbox list allows multiple selection, like [[listBox()]]. - * As a result, the corresponding submitted value is an array. - * @param string $name the name attribute of each checkbox. - * @param string|array $selection the selected value(s). - * @param array $items the data item used to generate the checkboxes. - * The array keys are the labels, while the array values are the corresponding checkbox values. - * Note that the labels will NOT be HTML-encoded, while the values will. - * @param array $options options (name => config) for the checkbox list. The following options are supported: - * - * - unselect: string, the value that should be submitted when none of the checkboxes is selected. - * By setting this option, a hidden input will be generated. - * - separator: string, the HTML code that separates items. - * - item: callable, a callback that can be used to customize the generation of the HTML code - * corresponding to a single item in $items. The signature of this callback must be: - * - * ~~~ - * function ($index, $label, $name, $checked, $value) - * ~~~ - * - * where $index is the zero-based index of the checkbox in the whole list; $label - * is the label for the checkbox; and $name, $value and $checked represent the name, - * value and the checked status of the checkbox input. - * @return string the generated checkbox list - */ - public static function checkboxList($name, $selection = null, $items = array(), $options = array()) - { - if (substr($name, -2) !== '[]') { - $name .= '[]'; - } - - $formatter = isset($options['item']) ? $options['item'] : null; - $lines = array(); - $index = 0; - foreach ($items as $value => $label) { - $checked = $selection !== null && - (!is_array($selection) && !strcmp($value, $selection) - || is_array($selection) && in_array($value, $selection)); - if ($formatter !== null) { - $lines[] = call_user_func($formatter, $index, $label, $name, $checked, $value); - } else { - $lines[] = static::label(static::checkbox($name, $checked, $value) . ' ' . $label); - } - $index++; - } - - if (isset($options['unselect'])) { - // add a hidden field so that if the list box has no option being selected, it still submits a value - $name2 = substr($name, -2) === '[]' ? substr($name, 0, -2) : $name; - $hidden = static::hiddenInput($name2, $options['unselect']); - } else { - $hidden = ''; - } - $separator = isset($options['separator']) ? $options['separator'] : "\n"; - - return $hidden . implode($separator, $lines); - } - - /** - * Generates a list of radio buttons. - * A radio button list is like a checkbox list, except that it only allows single selection. - * @param string $name the name attribute of each radio button. - * @param string|array $selection the selected value(s). - * @param array $items the data item used to generate the radio buttons. - * The array keys are the labels, while the array values are the corresponding radio button values. - * Note that the labels will NOT be HTML-encoded, while the values will. - * @param array $options options (name => config) for the radio button list. The following options are supported: - * - * - unselect: string, the value that should be submitted when none of the radio buttons is selected. - * By setting this option, a hidden input will be generated. - * - separator: string, the HTML code that separates items. - * - item: callable, a callback that can be used to customize the generation of the HTML code - * corresponding to a single item in $items. The signature of this callback must be: - * - * ~~~ - * function ($index, $label, $name, $checked, $value) - * ~~~ - * - * where $index is the zero-based index of the radio button in the whole list; $label - * is the label for the radio button; and $name, $value and $checked represent the name, - * value and the checked status of the radio button input. - * @return string the generated radio button list - */ - public static function radioList($name, $selection = null, $items = array(), $options = array()) - { - $formatter = isset($options['item']) ? $options['item'] : null; - $lines = array(); - $index = 0; - foreach ($items as $value => $label) { - $checked = $selection !== null && - (!is_array($selection) && !strcmp($value, $selection) - || is_array($selection) && in_array($value, $selection)); - if ($formatter !== null) { - $lines[] = call_user_func($formatter, $index, $label, $name, $checked, $value); - } else { - $lines[] = static::label(static::radio($name, $checked, $value) . ' ' . $label); - } - $index++; - } - - $separator = isset($options['separator']) ? $options['separator'] : "\n"; - if (isset($options['unselect'])) { - // add a hidden field so that if the list box has no option being selected, it still submits a value - $hidden = static::hiddenInput($name, $options['unselect']); - } else { - $hidden = ''; - } - - return $hidden . implode($separator, $lines); - } - - /** - * Renders the option tags that can be used by [[dropDownList()]] and [[listBox()]]. - * @param string|array $selection the selected value(s). This can be either a string for single selection - * or an array for multiple selections. - * @param array $items the option data items. The array keys are option values, and the array values - * are the corresponding option labels. The array can also be nested (i.e. some array values are arrays too). - * For each sub-array, an option group will be generated whose label is the key associated with the sub-array. - * If you have a list of data models, you may convert them into the format described above using - * [[\yii\helpers\ArrayHelper::map()]]. - * - * Note, the values and labels will be automatically HTML-encoded by this method, and the blank spaces in - * the labels will also be HTML-encoded. - * @param array $tagOptions the $options parameter that is passed to the [[dropDownList()]] or [[listBox()]] call. - * This method will take out these elements, if any: "prompt", "options" and "groups". See more details - * in [[dropDownList()]] for the explanation of these elements. - * - * @return string the generated list options - */ - public static function renderSelectOptions($selection, $items, &$tagOptions = array()) - { - $lines = array(); - if (isset($tagOptions['prompt'])) { - $prompt = str_replace(' ', ' ', static::encode($tagOptions['prompt'])); - $lines[] = static::tag('option', $prompt, array('value' => '')); - } - - $options = isset($tagOptions['options']) ? $tagOptions['options'] : array(); - $groups = isset($tagOptions['groups']) ? $tagOptions['groups'] : array(); - unset($tagOptions['prompt'], $tagOptions['options'], $tagOptions['groups']); - - foreach ($items as $key => $value) { - if (is_array($value)) { - $groupAttrs = isset($groups[$key]) ? $groups[$key] : array(); - $groupAttrs['label'] = $key; - $attrs = array('options' => $options, 'groups' => $groups); - $content = static::renderSelectOptions($selection, $value, $attrs); - $lines[] = static::tag('optgroup', "\n" . $content . "\n", $groupAttrs); - } else { - $attrs = isset($options[$key]) ? $options[$key] : array(); - $attrs['value'] = $key; - $attrs['selected'] = $selection !== null && - (!is_array($selection) && !strcmp($key, $selection) - || is_array($selection) && in_array($key, $selection)); - $lines[] = static::tag('option', str_replace(' ', ' ', static::encode($value)), $attrs); - } - } - - return implode("\n", $lines); - } - - /** - * Renders the HTML tag attributes. - * Boolean attributes such as s 'checked', 'disabled', 'readonly', will be handled specially - * according to [[booleanAttributes]] and [[showBooleanAttributeValues]]. - * @param array $attributes attributes to be rendered. The attribute values will be HTML-encoded using [[encode()]]. - * Attributes whose value is null will be ignored and not put in the rendering result. - * @return string the rendering result. If the attributes are not empty, they will be rendered - * into a string with a leading white space (such that it can be directly appended to the tag name - * in a tag. If there is no attribute, an empty string will be returned. - */ - public static function renderTagAttributes($attributes) - { - if (count($attributes) > 1) { - $sorted = array(); - foreach (static::$attributeOrder as $name) { - if (isset($attributes[$name])) { - $sorted[$name] = $attributes[$name]; - } - } - $attributes = array_merge($sorted, $attributes); - } - - $html = ''; - foreach ($attributes as $name => $value) { - if (isset(static::$booleanAttributes[strtolower($name)])) { - if ($value || strcasecmp($name, $value) === 0) { - $html .= static::$showBooleanAttributeValues ? " $name=\"$name\"" : " $name"; - } - } elseif ($value !== null) { - $html .= " $name=\"" . static::encode($value) . '"'; - } - } - return $html; - } - - /** - * Normalizes the input parameter to be a valid URL. - * - * If the input parameter - * - * - is an empty string: the currently requested URL will be returned; - * - is a non-empty string: it will be processed by [[Yii::getAlias()]] and returned; - * - is an array: the first array element is considered a route, while the rest of the name-value - * pairs are treated as the parameters to be used for URL creation using [[\yii\web\Controller::createUrl()]]. - * For example: `array('post/index', 'page' => 2)`, `array('index')`. - * - * @param array|string $url the parameter to be used to generate a valid URL - * @return string the normalized URL - * @throws InvalidParamException if the parameter is invalid. - */ - public static function url($url) - { - if (is_array($url)) { - if (isset($url[0])) { - $route = $url[0]; - $params = array_splice($url, 1); - if (Yii::$app->controller !== null) { - return Yii::$app->controller->createUrl($route, $params); - } else { - return Yii::$app->getUrlManager()->createUrl($route, $params); - } - } else { - throw new InvalidParamException('The array specifying a URL must contain at least one element.'); - } - } elseif ($url === '') { - return Yii::$app->getRequest()->getUrl(); - } else { - return Yii::getAlias($url); - } - } } diff --git a/framework/helpers/SecurityHelper.php b/framework/helpers/SecurityHelper.php index 5029dd6..d3cb2ad 100644 --- a/framework/helpers/SecurityHelper.php +++ b/framework/helpers/SecurityHelper.php @@ -7,11 +7,6 @@ namespace yii\helpers; -use Yii; -use yii\base\Exception; -use yii\base\InvalidConfigException; -use yii\base\InvalidParamException; - /** * SecurityHelper provides a set of methods to handle common security-related tasks. * @@ -29,244 +24,6 @@ use yii\base\InvalidParamException; * @author Tom Worster * @since 2.0 */ -class SecurityHelper +class SecurityHelper extends base\SecurityHelper { - /** - * Encrypts data. - * @param string $data data to be encrypted. - * @param string $key the encryption secret key - * @return string the encrypted data - * @throws Exception if PHP Mcrypt extension is not loaded or failed to be initialized - * @see decrypt() - */ - public static function encrypt($data, $key) - { - $module = static::openCryptModule(); - $key = StringHelper::substr($key, 0, mcrypt_enc_get_key_size($module)); - srand(); - $iv = mcrypt_create_iv(mcrypt_enc_get_iv_size($module), MCRYPT_RAND); - mcrypt_generic_init($module, $key, $iv); - $encrypted = $iv . mcrypt_generic($module, $data); - mcrypt_generic_deinit($module); - mcrypt_module_close($module); - return $encrypted; - } - - /** - * Decrypts data - * @param string $data data to be decrypted. - * @param string $key the decryption secret key - * @return string the decrypted data - * @throws Exception if PHP Mcrypt extension is not loaded or failed to be initialized - * @see encrypt() - */ - public static function decrypt($data, $key) - { - $module = static::openCryptModule(); - $key = StringHelper::substr($key, 0, mcrypt_enc_get_key_size($module)); - $ivSize = mcrypt_enc_get_iv_size($module); - $iv = StringHelper::substr($data, 0, $ivSize); - mcrypt_generic_init($module, $key, $iv); - $decrypted = mdecrypt_generic($module, StringHelper::substr($data, $ivSize, StringHelper::strlen($data))); - mcrypt_generic_deinit($module); - mcrypt_module_close($module); - return rtrim($decrypted, "\0"); - } - - /** - * Prefixes data with a keyed hash value so that it can later be detected if it is tampered. - * @param string $data the data to be protected - * @param string $key the secret key to be used for generating hash - * @param string $algorithm the hashing algorithm (e.g. "md5", "sha1", "sha256", etc.). Call PHP "hash_algos()" - * function to see the supported hashing algorithms on your system. - * @return string the data prefixed with the keyed hash - * @see validateData() - * @see getSecretKey() - */ - public static function hashData($data, $key, $algorithm = 'sha256') - { - return hash_hmac($algorithm, $data, $key) . $data; - } - - /** - * Validates if the given data is tampered. - * @param string $data the data to be validated. The data must be previously - * generated by [[hashData()]]. - * @param string $key the secret key that was previously used to generate the hash for the data in [[hashData()]]. - * @param string $algorithm the hashing algorithm (e.g. "md5", "sha1", "sha256", etc.). Call PHP "hash_algos()" - * function to see the supported hashing algorithms on your system. This must be the same - * as the value passed to [[hashData()]] when generating the hash for the data. - * @return string the real data with the hash stripped off. False if the data is tampered. - * @see hashData() - */ - public static function validateData($data, $key, $algorithm = 'sha256') - { - $hashSize = StringHelper::strlen(hash_hmac($algorithm, 'test', $key)); - $n = StringHelper::strlen($data); - if ($n >= $hashSize) { - $hash = StringHelper::substr($data, 0, $hashSize); - $data2 = StringHelper::substr($data, $hashSize, $n - $hashSize); - return $hash === hash_hmac($algorithm, $data2, $key) ? $data2 : false; - } else { - return false; - } - } - - /** - * Returns a secret key associated with the specified name. - * If the secret key does not exist, a random key will be generated - * and saved in the file "keys.php" under the application's runtime directory - * so that the same secret key can be returned in future requests. - * @param string $name the name that is associated with the secret key - * @param integer $length the length of the key that should be generated if not exists - * @return string the secret key associated with the specified name - */ - public static function getSecretKey($name, $length = 32) - { - static $keys; - $keyFile = Yii::$app->getRuntimePath() . '/keys.php'; - if ($keys === null) { - $keys = is_file($keyFile) ? require($keyFile) : array(); - } - if (!isset($keys[$name])) { - // generate a 32-char random key - $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; - $keys[$name] = substr(str_shuffle(str_repeat($chars, 5)), 0, $length); - file_put_contents($keyFile, " 30) { - throw new InvalidParamException('Hash is invalid.'); - } - - $test = crypt($password, $hash); - $n = strlen($test); - if (strlen($test) < 32 || $n !== strlen($hash)) { - return false; - } - - // Use a for-loop to compare two strings to prevent timing attacks. See: - // http://codereview.stackexchange.com/questions/13512 - $check = 0; - for ($i = 0; $i < $n; ++$i) { - $check |= (ord($test[$i]) ^ ord($hash[$i])); - } - - return $check === 0; - } - - /** - * Generates a salt that can be used to generate a password hash. - * - * The PHP [crypt()](http://php.net/manual/en/function.crypt.php) built-in function - * requires, for the Blowfish hash algorithm, a salt string in a specific format: - * "$2a$", "$2x$" or "$2y$", a two digit cost parameter, "$", and 22 characters - * from the alphabet "./0-9A-Za-z". - * - * @param integer $cost the cost parameter - * @return string the random salt value. - * @throws InvalidParamException if the cost parameter is not between 4 and 30 - */ - protected static function generateSalt($cost = 13) - { - $cost = (int)$cost; - if ($cost < 4 || $cost > 30) { - throw new InvalidParamException('Cost must be between 4 and 31.'); - } - - // Get 20 * 8bits of pseudo-random entropy from mt_rand(). - $rand = ''; - for ($i = 0; $i < 20; ++$i) { - $rand .= chr(mt_rand(0, 255)); - } - - // Add the microtime for a little more entropy. - $rand .= microtime(); - // Mix the bits cryptographically into a 20-byte binary string. - $rand = sha1($rand, true); - // Form the prefix that specifies Blowfish algorithm and cost parameter. - $salt = sprintf("$2y$%02d$", $cost); - // Append the random salt data in the required base64 format. - $salt .= str_replace('+', '.', substr(base64_encode($rand), 0, 22)); - return $salt; - } } \ No newline at end of file diff --git a/framework/helpers/StringHelper.php b/framework/helpers/StringHelper.php index ace34db..22b881a 100644 --- a/framework/helpers/StringHelper.php +++ b/framework/helpers/StringHelper.php @@ -14,112 +14,6 @@ namespace yii\helpers; * @author Alex Makarov * @since 2.0 */ -class StringHelper +class StringHelper extends base\StringHelper { - /** - * Returns the number of bytes in the given string. - * This method ensures the string is treated as a byte array. - * It will use `mb_strlen()` if it is available. - * @param string $string the string being measured for length - * @return integer the number of bytes in the given string. - */ - public static function strlen($string) - { - return function_exists('mb_strlen') ? mb_strlen($string, '8bit') : strlen($string); - } - - /** - * Returns the portion of string specified by the start and length parameters. - * This method ensures the string is treated as a byte array. - * It will use `mb_substr()` if it is available. - * @param string $string the input string. Must be one character or longer. - * @param integer $start the starting position - * @param integer $length the desired portion length - * @return string the extracted part of string, or FALSE on failure or an empty string. - * @see http://www.php.net/manual/en/function.substr.php - */ - public static function substr($string, $start, $length) - { - return function_exists('mb_substr') ? mb_substr($string, $start, $length, '8bit') : substr($string, $start, $length); - } - - /** - * Converts a word to its plural form. - * Note that this is for English only! - * For example, 'apple' will become 'apples', and 'child' will become 'children'. - * @param string $name the word to be pluralized - * @return string the pluralized word - */ - public static function pluralize($name) - { - static $rules = array( - '/(m)ove$/i' => '\1oves', - '/(f)oot$/i' => '\1eet', - '/(c)hild$/i' => '\1hildren', - '/(h)uman$/i' => '\1umans', - '/(m)an$/i' => '\1en', - '/(s)taff$/i' => '\1taff', - '/(t)ooth$/i' => '\1eeth', - '/(p)erson$/i' => '\1eople', - '/([m|l])ouse$/i' => '\1ice', - '/(x|ch|ss|sh|us|as|is|os)$/i' => '\1es', - '/([^aeiouy]|qu)y$/i' => '\1ies', - '/(?:([^f])fe|([lr])f)$/i' => '\1\2ves', - '/(shea|lea|loa|thie)f$/i' => '\1ves', - '/([ti])um$/i' => '\1a', - '/(tomat|potat|ech|her|vet)o$/i' => '\1oes', - '/(bu)s$/i' => '\1ses', - '/(ax|test)is$/i' => '\1es', - '/s$/' => 's', - ); - foreach ($rules as $rule => $replacement) { - if (preg_match($rule, $name)) { - return preg_replace($rule, $replacement, $name); - } - } - return $name . 's'; - } - - /** - * Converts a CamelCase name into space-separated words. - * For example, 'PostTag' will be converted to 'Post Tag'. - * @param string $name the string to be converted - * @param boolean $ucwords whether to capitalize the first letter in each word - * @return string the resulting words - */ - public static function camel2words($name, $ucwords = true) - { - $label = trim(strtolower(str_replace(array('-', '_', '.'), ' ', preg_replace('/(? * @since 2.0 */ -class CVarDumper +class VarDumper extends base\VarDumper { - private static $_objects; - private static $_output; - private static $_depth; - - /** - * Displays a variable. - * This method achieves the similar functionality as var_dump and print_r - * but is more robust when handling complex objects such as Yii controllers. - * @param mixed $var variable to be dumped - * @param integer $depth maximum depth that the dumper should go into the variable. Defaults to 10. - * @param boolean $highlight whether the result should be syntax-highlighted - */ - public static function dump($var, $depth = 10, $highlight = false) - { - echo self::dumpAsString($var, $depth, $highlight); - } - - /** - * Dumps a variable in terms of a string. - * This method achieves the similar functionality as var_dump and print_r - * but is more robust when handling complex objects such as Yii controllers. - * @param mixed $var variable to be dumped - * @param integer $depth maximum depth that the dumper should go into the variable. Defaults to 10. - * @param boolean $highlight whether the result should be syntax-highlighted - * @return string the string representation of the variable - */ - public static function dumpAsString($var, $depth = 10, $highlight = false) - { - self::$_output = ''; - self::$_objects = array(); - self::$_depth = $depth; - self::dumpInternal($var, 0); - if ($highlight) { - $result = highlight_string("/', '', $result, 1); - } - return self::$_output; - } - - /* - * @param mixed $var variable to be dumped - * @param integer $level depth level - */ - private static function dumpInternal($var, $level) - { - switch (gettype($var)) { - case 'boolean': - self::$_output .= $var ? 'true' : 'false'; - break; - case 'integer': - self::$_output .= "$var"; - break; - case 'double': - self::$_output .= "$var"; - break; - case 'string': - self::$_output .= "'" . addslashes($var) . "'"; - break; - case 'resource': - self::$_output .= '{resource}'; - break; - case 'NULL': - self::$_output .= "null"; - break; - case 'unknown type': - self::$_output .= '{unknown}'; - break; - case 'array': - if (self::$_depth <= $level) { - self::$_output .= 'array(...)'; - } elseif (empty($var)) { - self::$_output .= 'array()'; - } else { - $keys = array_keys($var); - $spaces = str_repeat(' ', $level * 4); - self::$_output .= "array\n" . $spaces . '('; - foreach ($keys as $key) { - self::$_output .= "\n" . $spaces . ' '; - self::dumpInternal($key, 0); - self::$_output .= ' => '; - self::dumpInternal($var[$key], $level + 1); - } - self::$_output .= "\n" . $spaces . ')'; - } - break; - case 'object': - if (($id = array_search($var, self::$_objects, true)) !== false) { - self::$_output .= get_class($var) . '#' . ($id + 1) . '(...)'; - } elseif (self::$_depth <= $level) { - self::$_output .= get_class($var) . '(...)'; - } else { - $id = self::$_objects[] = $var; - $className = get_class($var); - $members = (array)$var; - $spaces = str_repeat(' ', $level * 4); - self::$_output .= "$className#$id\n" . $spaces . '('; - foreach ($members as $key => $value) { - $keyDisplay = strtr(trim($key), array("\0" => ':')); - self::$_output .= "\n" . $spaces . " [$keyDisplay] => "; - self::dumpInternal($value, $level + 1); - } - self::$_output .= "\n" . $spaces . ')'; - } - break; - } - } } \ No newline at end of file diff --git a/framework/helpers/base/ArrayHelper.php b/framework/helpers/base/ArrayHelper.php new file mode 100644 index 0000000..9870542 --- /dev/null +++ b/framework/helpers/base/ArrayHelper.php @@ -0,0 +1,340 @@ + + * @since 2.0 + */ +class ArrayHelper +{ + /** + * Merges two or more arrays into one recursively. + * If each array has an element with the same string key value, the latter + * will overwrite the former (different from array_merge_recursive). + * Recursive merging will be conducted if both arrays have an element of array + * type and are having the same key. + * For integer-keyed elements, the elements from the latter array will + * be appended to the former array. + * @param array $a array to be merged to + * @param array $b array to be merged from. You can specify additional + * arrays via third argument, fourth argument etc. + * @return array the merged array (the original arrays are not changed.) + */ + public static function merge($a, $b) + { + $args = func_get_args(); + $res = array_shift($args); + while ($args !== array()) { + $next = array_shift($args); + foreach ($next as $k => $v) { + if (is_integer($k)) { + isset($res[$k]) ? $res[] = $v : $res[$k] = $v; + } elseif (is_array($v) && isset($res[$k]) && is_array($res[$k])) { + $res[$k] = self::merge($res[$k], $v); + } else { + $res[$k] = $v; + } + } + } + return $res; + } + + /** + * Retrieves the value of an array element or object property with the given key or property name. + * If the key does not exist in the array, the default value will be returned instead. + * + * Below are some usage examples, + * + * ~~~ + * // working with array + * $username = \yii\helpers\ArrayHelper::getValue($_POST, 'username'); + * // working with object + * $username = \yii\helpers\ArrayHelper::getValue($user, 'username'); + * // working with anonymous function + * $fullName = \yii\helpers\ArrayHelper::getValue($user, function($user, $defaultValue) { + * return $user->firstName . ' ' . $user->lastName; + * }); + * ~~~ + * + * @param array|object $array array or object to extract value from + * @param string|\Closure $key key name of the array element, or property name of the object, + * or an anonymous function returning the value. The anonymous function signature should be: + * `function($array, $defaultValue)`. + * @param mixed $default the default value to be returned if the specified key does not exist + * @return mixed the value of the + */ + public static function getValue($array, $key, $default = null) + { + if ($key instanceof \Closure) { + return $key($array, $default); + } elseif (is_array($array)) { + return isset($array[$key]) || array_key_exists($key, $array) ? $array[$key] : $default; + } else { + return $array->$key; + } + } + + /** + * Indexes an array according to a specified key. + * The input array should be multidimensional or an array of objects. + * + * The key can be a key name of the sub-array, a property name of object, or an anonymous + * function which returns the key value given an array element. + * + * If a key value is null, the corresponding array element will be discarded and not put in the result. + * + * For example, + * + * ~~~ + * $array = array( + * array('id' => '123', 'data' => 'abc'), + * array('id' => '345', 'data' => 'def'), + * ); + * $result = ArrayHelper::index($array, 'id'); + * // the result is: + * // array( + * // '123' => array('id' => '123', 'data' => 'abc'), + * // '345' => array('id' => '345', 'data' => 'def'), + * // ) + * + * // using anonymous function + * $result = ArrayHelper::index($array, function(element) { + * return $element['id']; + * }); + * ~~~ + * + * @param array $array the array that needs to be indexed + * @param string|\Closure $key the column name or anonymous function whose result will be used to index the array + * @return array the indexed array + */ + public static function index($array, $key) + { + $result = array(); + foreach ($array as $element) { + $value = static::getValue($element, $key); + $result[$value] = $element; + } + return $result; + } + + /** + * Returns the values of a specified column in an array. + * The input array should be multidimensional or an array of objects. + * + * For example, + * + * ~~~ + * $array = array( + * array('id' => '123', 'data' => 'abc'), + * array('id' => '345', 'data' => 'def'), + * ); + * $result = ArrayHelper::getColumn($array, 'id'); + * // the result is: array( '123', '345') + * + * // using anonymous function + * $result = ArrayHelper::getColumn($array, function(element) { + * return $element['id']; + * }); + * ~~~ + * + * @param array $array + * @param string|\Closure $name + * @param boolean $keepKeys whether to maintain the array keys. If false, the resulting array + * will be re-indexed with integers. + * @return array the list of column values + */ + public static function getColumn($array, $name, $keepKeys = true) + { + $result = array(); + if ($keepKeys) { + foreach ($array as $k => $element) { + $result[$k] = static::getValue($element, $name); + } + } else { + foreach ($array as $element) { + $result[] = static::getValue($element, $name); + } + } + + return $result; + } + + /** + * Builds a map (key-value pairs) from a multidimensional array or an array of objects. + * The `$from` and `$to` parameters specify the key names or property names to set up the map. + * Optionally, one can further group the map according to a grouping field `$group`. + * + * For example, + * + * ~~~ + * $array = array( + * array('id' => '123', 'name' => 'aaa', 'class' => 'x'), + * array('id' => '124', 'name' => 'bbb', 'class' => 'x'), + * array('id' => '345', 'name' => 'ccc', 'class' => 'y'), + * ); + * + * $result = ArrayHelper::map($array, 'id', 'name'); + * // the result is: + * // array( + * // '123' => 'aaa', + * // '124' => 'bbb', + * // '345' => 'ccc', + * // ) + * + * $result = ArrayHelper::map($array, 'id', 'name', 'class'); + * // the result is: + * // array( + * // 'x' => array( + * // '123' => 'aaa', + * // '124' => 'bbb', + * // ), + * // 'y' => array( + * // '345' => 'ccc', + * // ), + * // ) + * ~~~ + * + * @param array $array + * @param string|\Closure $from + * @param string|\Closure $to + * @param string|\Closure $group + * @return array + */ + public static function map($array, $from, $to, $group = null) + { + $result = array(); + foreach ($array as $element) { + $key = static::getValue($element, $from); + $value = static::getValue($element, $to); + if ($group !== null) { + $result[static::getValue($element, $group)][$key] = $value; + } else { + $result[$key] = $value; + } + } + return $result; + } + + /** + * Sorts an array of objects or arrays (with the same structure) by one or several keys. + * @param array $array the array to be sorted. The array will be modified after calling this method. + * @param string|\Closure|array $key the key(s) to be sorted by. This refers to a key name of the sub-array + * elements, a property name of the objects, or an anonymous function returning the values for comparison + * purpose. The anonymous function signature should be: `function($item)`. + * To sort by multiple keys, provide an array of keys here. + * @param boolean|array $ascending whether to sort in ascending or descending order. When + * sorting by multiple keys with different ascending orders, use an array of ascending flags. + * @param integer|array $sortFlag the PHP sort flag. Valid values include: + * `SORT_REGULAR`, `SORT_NUMERIC`, `SORT_STRING`, and `SORT_STRING | SORT_FLAG_CASE`. The last + * value is for sorting strings in case-insensitive manner. Please refer to + * See [PHP manual](http://php.net/manual/en/function.sort.php) for more details. + * When sorting by multiple keys with different sort flags, use an array of sort flags. + * @throws InvalidParamException if the $ascending or $sortFlag parameters do not have + * correct number of elements as that of $key. + */ + public static function multisort(&$array, $key, $ascending = true, $sortFlag = SORT_REGULAR) + { + $keys = is_array($key) ? $key : array($key); + if (empty($keys) || empty($array)) { + return; + } + $n = count($keys); + if (is_scalar($ascending)) { + $ascending = array_fill(0, $n, $ascending); + } elseif (count($ascending) !== $n) { + throw new InvalidParamException('The length of $ascending parameter must be the same as that of $keys.'); + } + if (is_scalar($sortFlag)) { + $sortFlag = array_fill(0, $n, $sortFlag); + } elseif (count($sortFlag) !== $n) { + throw new InvalidParamException('The length of $ascending parameter must be the same as that of $keys.'); + } + $args = array(); + foreach ($keys as $i => $key) { + $flag = $sortFlag[$i]; + if ($flag == (SORT_STRING | SORT_FLAG_CASE)) { + $flag = SORT_STRING; + $column = array(); + foreach (static::getColumn($array, $key) as $k => $value) { + $column[$k] = strtolower($value); + } + $args[] = $column; + } else { + $args[] = static::getColumn($array, $key); + } + $args[] = $ascending[$i] ? SORT_ASC : SORT_DESC; + $args[] = $flag; + } + $args[] = &$array; + call_user_func_array('array_multisort', $args); + } + + /** + * Encodes special characters in an array of strings into HTML entities. + * Both the array keys and values will be encoded. + * If a value is an array, this method will also encode it recursively. + * @param array $data data to be encoded + * @param boolean $valuesOnly whether to encode array values only. If false, + * both the array keys and array values will be encoded. + * @param string $charset the charset that the data is using. If not set, + * [[\yii\base\Application::charset]] will be used. + * @return array the encoded data + * @see http://www.php.net/manual/en/function.htmlspecialchars.php + */ + public static function htmlEncode($data, $valuesOnly = true, $charset = null) + { + if ($charset === null) { + $charset = Yii::$app->charset; + } + $d = array(); + foreach ($data as $key => $value) { + if (!$valuesOnly && is_string($key)) { + $key = htmlspecialchars($key, ENT_QUOTES, $charset); + } + if (is_string($value)) { + $d[$key] = htmlspecialchars($value, ENT_QUOTES, $charset); + } elseif (is_array($value)) { + $d[$key] = static::htmlEncode($value, $charset); + } + } + return $d; + } + + /** + * Decodes HTML entities into the corresponding characters in an array of strings. + * Both the array keys and values will be decoded. + * If a value is an array, this method will also decode it recursively. + * @param array $data data to be decoded + * @param boolean $valuesOnly whether to decode array values only. If false, + * both the array keys and array values will be decoded. + * @return array the decoded data + * @see http://www.php.net/manual/en/function.htmlspecialchars-decode.php + */ + public static function htmlDecode($data, $valuesOnly = true) + { + $d = array(); + foreach ($data as $key => $value) { + if (!$valuesOnly && is_string($key)) { + $key = htmlspecialchars_decode($key, ENT_QUOTES); + } + if (is_string($value)) { + $d[$key] = htmlspecialchars_decode($value, ENT_QUOTES); + } elseif (is_array($value)) { + $d[$key] = static::htmlDecode($value); + } + } + return $d; + } +} \ No newline at end of file diff --git a/framework/helpers/base/ConsoleColor.php b/framework/helpers/base/ConsoleColor.php new file mode 100644 index 0000000..5e7f577 --- /dev/null +++ b/framework/helpers/base/ConsoleColor.php @@ -0,0 +1,470 @@ + + * @since 2.0 + */ +class ConsoleColor +{ + const FG_BLACK = 30; + const FG_RED = 31; + const FG_GREEN = 32; + const FG_YELLOW = 33; + const FG_BLUE = 34; + const FG_PURPLE = 35; + const FG_CYAN = 36; + const FG_GREY = 37; + + const BG_BLACK = 40; + const BG_RED = 41; + const BG_GREEN = 42; + const BG_YELLOW = 43; + const BG_BLUE = 44; + const BG_PURPLE = 45; + const BG_CYAN = 46; + const BG_GREY = 47; + + const BOLD = 1; + const ITALIC = 3; + const UNDERLINE = 4; + const BLINK = 5; + const NEGATIVE = 7; + const CONCEALED = 8; + const CROSSED_OUT = 9; + const FRAMED = 51; + const ENCIRCLED = 52; + const OVERLINED = 53; + + /** + * Moves the terminal cursor up by sending ANSI control code CUU to the terminal. + * If the cursor is already at the edge of the screen, this has no effect. + * @param integer $rows number of rows the cursor should be moved up + */ + public static function moveCursorUp($rows=1) + { + echo "\033[" . (int) $rows . 'A'; + } + + /** + * Moves the terminal cursor down by sending ANSI control code CUD to the terminal. + * If the cursor is already at the edge of the screen, this has no effect. + * @param integer $rows number of rows the cursor should be moved down + */ + public static function moveCursorDown($rows=1) + { + echo "\033[" . (int) $rows . 'B'; + } + + /** + * Moves the terminal cursor forward by sending ANSI control code CUF to the terminal. + * If the cursor is already at the edge of the screen, this has no effect. + * @param integer $steps number of steps the cursor should be moved forward + */ + public static function moveCursorForward($steps=1) + { + echo "\033[" . (int) $steps . 'C'; + } + + /** + * Moves the terminal cursor backward by sending ANSI control code CUB to the terminal. + * If the cursor is already at the edge of the screen, this has no effect. + * @param integer $steps number of steps the cursor should be moved backward + */ + public static function moveCursorBackward($steps=1) + { + echo "\033[" . (int) $steps . 'D'; + } + + /** + * Moves the terminal cursor to the beginning of the next line by sending ANSI control code CNL to the terminal. + * @param integer $lines number of lines the cursor should be moved down + */ + public static function moveCursorNextLine($lines=1) + { + echo "\033[" . (int) $lines . 'E'; + } + + /** + * Moves the terminal cursor to the beginning of the previous line by sending ANSI control code CPL to the terminal. + * @param integer $lines number of lines the cursor should be moved up + */ + public static function moveCursorPrevLine($lines=1) + { + echo "\033[" . (int) $lines . 'F'; + } + + /** + * Moves the cursor to an absolute position given as column and row by sending ANSI control code CUP or CHA to the terminal. + * @param integer $column 1-based column number, 1 is the left edge of the screen. + * @param integer|null $row 1-based row number, 1 is the top edge of the screen. if not set, will move cursor only in current line. + */ + public static function moveCursorTo($column, $row=null) + { + if ($row === null) { + echo "\033[" . (int) $column . 'G'; + } else { + echo "\033[" . (int) $row . ';' . (int) $column . 'H'; + } + } + + /** + * Scrolls whole page up by sending ANSI control code SU to the terminal. + * New lines are added at the bottom. This is not supported by ANSI.SYS used in windows. + * @param int $lines number of lines to scroll up + */ + public static function scrollUp($lines=1) + { + echo "\033[".(int)$lines."S"; + } + + /** + * Scrolls whole page down by sending ANSI control code SD to the terminal. + * New lines are added at the top. This is not supported by ANSI.SYS used in windows. + * @param int $lines number of lines to scroll down + */ + public static function scrollDown($lines=1) + { + echo "\033[".(int)$lines."T"; + } + + /** + * Saves the current cursor position by sending ANSI control code SCP to the terminal. + * Position can then be restored with {@link restoreCursorPosition}. + */ + public static function saveCursorPosition() + { + echo "\033[s"; + } + + /** + * Restores the cursor position saved with {@link saveCursorPosition} by sending ANSI control code RCP to the terminal. + */ + public static function restoreCursorPosition() + { + echo "\033[u"; + } + + /** + * Hides the cursor by sending ANSI DECTCEM code ?25l to the terminal. + * Use {@link showCursor} to bring it back. + * Do not forget to show cursor when your application exits. Cursor might stay hidden in terminal after exit. + */ + public static function hideCursor() + { + echo "\033[?25l"; + } + + /** + * Will show a cursor again when it has been hidden by {@link hideCursor} by sending ANSI DECTCEM code ?25h to the terminal. + */ + public static function showCursor() + { + echo "\033[?25h"; + } + + /** + * Clears entire screen content by sending ANSI control code ED with argument 2 to the terminal. + * Cursor position will not be changed. + * **Note:** ANSI.SYS implementation used in windows will reset cursor position to upper left corner of the screen. + */ + public static function clearScreen() + { + echo "\033[2J"; + } + + /** + * Clears text from cursor to the beginning of the screen by sending ANSI control code ED with argument 1 to the terminal. + * Cursor position will not be changed. + */ + public static function clearScreenBeforeCursor() + { + echo "\033[1J"; + } + + /** + * Clears text from cursor to the end of the screen by sending ANSI control code ED with argument 0 to the terminal. + * Cursor position will not be changed. + */ + public static function clearScreenAfterCursor() + { + echo "\033[0J"; + } + + /** + * Clears the line, the cursor is currently on by sending ANSI control code EL with argument 2 to the terminal. + * Cursor position will not be changed. + */ + public static function clearLine() + { + echo "\033[2K"; + } + + /** + * Clears text from cursor position to the beginning of the line by sending ANSI control code EL with argument 1 to the terminal. + * Cursor position will not be changed. + */ + public static function clearLineBeforeCursor() + { + echo "\033[1K"; + } + + /** + * Clears text from cursor position to the end of the line by sending ANSI control code EL with argument 0 to the terminal. + * Cursor position will not be changed. + */ + public static function clearLineAfterCursor() + { + echo "\033[0K"; + } + + /** + * Will send ANSI format for following output + * + * You can pass any of the FG_*, BG_* and TEXT_* constants and also xterm256ColorBg + * TODO: documentation + */ + public static function ansiStyle() + { + echo "\033[" . implode(';', func_get_args()) . 'm'; + } + + /** + * Will return a string formatted with the given ANSI style + * + * See {@link ansiStyle} for possible arguments. + * @param string $string the string to be formatted + * @return string + */ + public static function ansiStyleString($string) + { + $args = func_get_args(); + array_shift($args); + $code = implode(';', $args); + return "\033[0m" . ($code !== '' ? "\033[" . $code . "m" : '') . $string."\033[0m"; + } + + //const COLOR_XTERM256 = 38;// http://en.wikipedia.org/wiki/Talk:ANSI_escape_code#xterm-256colors + public static function xterm256ColorFg($i) // TODO naming! + { + return '38;5;'.$i; + } + + public static function xterm256ColorBg($i) // TODO naming! + { + return '48;5;'.$i; + } + + /** + * Usage: list($w, $h) = ConsoleHelper::getScreenSize(); + * + * @return array + */ + public static function getScreenSize() + { + // TODO implement + return array(150,50); + } + + /** + * resets any ansi style set by previous method {@link ansiStyle} + * Any output after this is will have default text style. + */ + public static function reset() + { + echo "\033[0m"; + } + + /** + * Strips ANSI control codes from a string + * + * @param string $string String to strip + * @return string + */ + public static function strip($string) + { + return preg_replace('/\033\[[\d;]+m/', '', $string); // TODO currently only strips color + } + + // TODO refactor and review + public static function ansiToHtml($string) + { + $tags = 0; + return preg_replace_callback('/\033\[[\d;]+m/', function($ansi) use (&$tags) { + $styleA = array(); + foreach(explode(';', $ansi) as $controlCode) + { + switch($controlCode) + { + case static::FG_BLACK: $style = array('color' => '#000000'); break; + case static::FG_BLUE: $style = array('color' => '#000078'); break; + case static::FG_CYAN: $style = array('color' => '#007878'); break; + case static::FG_GREEN: $style = array('color' => '#007800'); break; + case static::FG_GREY: $style = array('color' => '#787878'); break; + case static::FG_PURPLE: $style = array('color' => '#780078'); break; + case static::FG_RED: $style = array('color' => '#780000'); break; + case static::FG_YELLOW: $style = array('color' => '#787800'); break; + case static::BG_BLACK: $style = array('background-color' => '#000000'); break; + case static::BG_BLUE: $style = array('background-color' => '#000078'); break; + case static::BG_CYAN: $style = array('background-color' => '#007878'); break; + case static::BG_GREEN: $style = array('background-color' => '#007800'); break; + case static::BG_GREY: $style = array('background-color' => '#787878'); break; + case static::BG_PURPLE: $style = array('background-color' => '#780078'); break; + case static::BG_RED: $style = array('background-color' => '#780000'); break; + case static::BG_YELLOW: $style = array('background-color' => '#787800'); break; + case static::BOLD: $style = array('font-weight' => 'bold'); break; + case static::ITALIC: $style = array('font-style' => 'italic'); break; + case static::UNDERLINE: $style = array('text-decoration' => array('underline')); break; + case static::OVERLINED: $style = array('text-decoration' => array('overline')); break; + case static::CROSSED_OUT:$style = array('text-decoration' => array('line-through')); break; + case static::BLINK: $style = array('text-decoration' => array('blink')); break; + case static::NEGATIVE: // ??? + case static::CONCEALED: + case static::ENCIRCLED: + case static::FRAMED: + // TODO allow resetting codes + break; + case 0: // ansi reset + $return = ''; + for($n=$tags; $tags>0; $tags--) { + $return .= ''; + } + return $return; + } + + $styleA = ArrayHelper::merge($styleA, $style); + } + $styleString[] = array(); + foreach($styleA as $name => $content) { + if ($name === 'text-decoration') { + $content = implode(' ', $content); + } + $styleString[] = $name.':'.$content; + } + $tags++; + return ' $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/base/Html.php b/framework/helpers/base/Html.php new file mode 100644 index 0000000..5e7f4ad --- /dev/null +++ b/framework/helpers/base/Html.php @@ -0,0 +1,981 @@ + + * @since 2.0 + */ +class Html +{ + /** + * @var boolean whether to close void (empty) elements. Defaults to true. + * @see voidElements + */ + public static $closeVoidElements = true; + /** + * @var array list of void elements (element name => 1) + * @see closeVoidElements + * @see http://www.w3.org/TR/html-markup/syntax.html#void-element + */ + public static $voidElements = array( + 'area' => 1, + 'base' => 1, + 'br' => 1, + 'col' => 1, + 'command' => 1, + 'embed' => 1, + 'hr' => 1, + 'img' => 1, + 'input' => 1, + 'keygen' => 1, + 'link' => 1, + 'meta' => 1, + 'param' => 1, + 'source' => 1, + 'track' => 1, + 'wbr' => 1, + ); + /** + * @var boolean whether to show the values of boolean attributes in element tags. + * If false, only the attribute names will be generated. + * @see booleanAttributes + */ + public static $showBooleanAttributeValues = true; + /** + * @var array list of boolean attributes. The presence of a boolean attribute on + * an element represents the true value, and the absence of the attribute represents the false value. + * @see showBooleanAttributeValues + * @see http://www.w3.org/TR/html5/infrastructure.html#boolean-attributes + */ + public static $booleanAttributes = array( + 'async' => 1, + 'autofocus' => 1, + 'autoplay' => 1, + 'checked' => 1, + 'controls' => 1, + 'declare' => 1, + 'default' => 1, + 'defer' => 1, + 'disabled' => 1, + 'formnovalidate' => 1, + 'hidden' => 1, + 'ismap' => 1, + 'loop' => 1, + 'multiple' => 1, + 'muted' => 1, + 'nohref' => 1, + 'noresize' => 1, + 'novalidate' => 1, + 'open' => 1, + 'readonly' => 1, + 'required' => 1, + 'reversed' => 1, + 'scoped' => 1, + 'seamless' => 1, + 'selected' => 1, + 'typemustmatch' => 1, + ); + /** + * @var array the preferred order of attributes in a tag. This mainly affects the order of the attributes + * that are rendered by [[renderAttributes()]]. + */ + public static $attributeOrder = array( + 'type', + 'id', + 'class', + 'name', + 'value', + + 'href', + 'src', + 'action', + 'method', + + 'selected', + 'checked', + 'readonly', + 'disabled', + 'multiple', + + 'size', + 'maxlength', + 'width', + 'height', + 'rows', + 'cols', + + 'alt', + 'title', + 'rel', + 'media', + ); + + /** + * Encodes special characters into HTML entities. + * The [[yii\base\Application::charset|application charset]] will be used for encoding. + * @param string $content the content to be encoded + * @return string the encoded content + * @see decode + * @see http://www.php.net/manual/en/function.htmlspecialchars.php + */ + public static function encode($content) + { + return htmlspecialchars($content, ENT_QUOTES, Yii::$app->charset); + } + + /** + * Decodes special HTML entities back to the corresponding characters. + * This is the opposite of [[encode()]]. + * @param string $content the content to be decoded + * @return string the decoded content + * @see encode + * @see http://www.php.net/manual/en/function.htmlspecialchars-decode.php + */ + public static function decode($content) + { + return htmlspecialchars_decode($content, ENT_QUOTES); + } + + /** + * Generates a complete HTML tag. + * @param string $name the tag name + * @param string $content the content to be enclosed between the start and end tags. It will not be HTML-encoded. + * If this is coming from end users, you should consider [[encode()]] it to prevent XSS attacks. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated HTML tag + * @see beginTag + * @see endTag + */ + public static function tag($name, $content = '', $options = array()) + { + $html = '<' . $name . static::renderTagAttributes($options); + if (isset(static::$voidElements[strtolower($name)])) { + return $html . (static::$closeVoidElements ? ' />' : '>'); + } else { + return $html . ">$content"; + } + } + + /** + * Generates a start tag. + * @param string $name the tag name + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated start tag + * @see endTag + * @see tag + */ + public static function beginTag($name, $options = array()) + { + return '<' . $name . static::renderTagAttributes($options) . '>'; + } + + /** + * Generates an end tag. + * @param string $name the tag name + * @return string the generated end tag + * @see beginTag + * @see tag + */ + public static function endTag($name) + { + return ""; + } + + /** + * Encloses the given content within a CDATA tag. + * @param string $content the content to be enclosed within the CDATA tag + * @return string the CDATA tag with the enclosed content. + */ + public static function cdata($content) + { + return ''; + } + + /** + * Generates a style tag. + * @param string $content the style content + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * If the options does not contain "type", a "type" attribute with value "text/css" will be used. + * @return string the generated style tag + */ + public static function style($content, $options = array()) + { + if (!isset($options['type'])) { + $options['type'] = 'text/css'; + } + return static::tag('style', "/**/", $options); + } + + /** + * Generates a script tag. + * @param string $content the script content + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * If the options does not contain "type", a "type" attribute with value "text/javascript" will be rendered. + * @return string the generated script tag + */ + public static function script($content, $options = array()) + { + if (!isset($options['type'])) { + $options['type'] = 'text/javascript'; + } + return static::tag('script', "/**/", $options); + } + + /** + * Generates a link tag that refers to an external CSS file. + * @param array|string $url the URL of the external CSS file. This parameter will be processed by [[url()]]. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated link tag + * @see url + */ + public static function cssFile($url, $options = array()) + { + $options['rel'] = 'stylesheet'; + $options['type'] = 'text/css'; + $options['href'] = static::url($url); + return static::tag('link', '', $options); + } + + /** + * Generates a script tag that refers to an external JavaScript file. + * @param string $url the URL of the external JavaScript file. This parameter will be processed by [[url()]]. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated script tag + * @see url + */ + public static function jsFile($url, $options = array()) + { + $options['type'] = 'text/javascript'; + $options['src'] = static::url($url); + return static::tag('script', '', $options); + } + + /** + * Generates a form start tag. + * @param array|string $action the form action URL. This parameter will be processed by [[url()]]. + * @param string $method the form submission method, either "post" or "get" (case-insensitive) + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated form start tag. + * @see endForm + */ + public static function beginForm($action = '', $method = 'post', $options = array()) + { + $action = static::url($action); + + // query parameters in the action are ignored for GET method + // we use hidden fields to add them back + $hiddens = array(); + if (!strcasecmp($method, 'get') && ($pos = strpos($action, '?')) !== false) { + foreach (explode('&', substr($action, $pos + 1)) as $pair) { + if (($pos1 = strpos($pair, '=')) !== false) { + $hiddens[] = static::hiddenInput(urldecode(substr($pair, 0, $pos1)), urldecode(substr($pair, $pos1 + 1))); + } else { + $hiddens[] = static::hiddenInput(urldecode($pair), ''); + } + } + $action = substr($action, 0, $pos); + } + + $options['action'] = $action; + $options['method'] = $method; + $form = static::beginTag('form', $options); + if ($hiddens !== array()) { + $form .= "\n" . implode("\n", $hiddens); + } + + return $form; + } + + /** + * Generates a form end tag. + * @return string the generated tag + * @see beginForm + */ + public static function endForm() + { + return ''; + } + + /** + * Generates a hyperlink tag. + * @param string $text link body. It will NOT be HTML-encoded. Therefore you can pass in HTML code + * such as an image tag. If this is is coming from end users, you should consider [[encode()]] + * it to prevent XSS attacks. + * @param array|string|null $url the URL for the hyperlink tag. This parameter will be processed by [[url()]] + * and will be used for the "href" attribute of the tag. If this parameter is null, the "href" attribute + * will not be generated. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated hyperlink + * @see url + */ + public static function a($text, $url = null, $options = array()) + { + if ($url !== null) { + $options['href'] = static::url($url); + } + return static::tag('a', $text, $options); + } + + /** + * Generates a mailto hyperlink. + * @param string $text link body. It will NOT be HTML-encoded. Therefore you can pass in HTML code + * such as an image tag. If this is is coming from end users, you should consider [[encode()]] + * it to prevent XSS attacks. + * @param string $email email address. If this is null, the first parameter (link body) will be treated + * as the email address and used. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated mailto link + */ + public static function mailto($text, $email = null, $options = array()) + { + return static::a($text, 'mailto:' . ($email === null ? $text : $email), $options); + } + + /** + * Generates an image tag. + * @param string $src the image URL. This parameter will be processed by [[url()]]. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated image tag + */ + public static function img($src, $options = array()) + { + $options['src'] = static::url($src); + if (!isset($options['alt'])) { + $options['alt'] = ''; + } + return static::tag('img', null, $options); + } + + /** + * Generates a label tag. + * @param string $content label text. It will NOT be HTML-encoded. Therefore you can pass in HTML code + * such as an image tag. If this is is coming from end users, you should consider [[encode()]] + * it to prevent XSS attacks. + * @param string $for the ID of the HTML element that this label is associated with. + * If this is null, the "for" attribute will not be generated. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated label tag + */ + public static function label($content, $for = null, $options = array()) + { + $options['for'] = $for; + return static::tag('label', $content, $options); + } + + /** + * Generates a button tag. + * @param string $name the name attribute. If it is null, the name attribute will not be generated. + * @param string $value the value attribute. If it is null, the value attribute will not be generated. + * @param string $content the content enclosed within the button tag. It will NOT be HTML-encoded. + * Therefore you can pass in HTML code such as an image tag. If this is is coming from end users, + * you should consider [[encode()]] it to prevent XSS attacks. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * If the options does not contain "type", a "type" attribute with value "button" will be rendered. + * @return string the generated button tag + */ + public static function button($name = null, $value = null, $content = 'Button', $options = array()) + { + $options['name'] = $name; + $options['value'] = $value; + if (!isset($options['type'])) { + $options['type'] = 'button'; + } + return static::tag('button', $content, $options); + } + + /** + * Generates a submit button tag. + * @param string $name the name attribute. If it is null, the name attribute will not be generated. + * @param string $value the value attribute. If it is null, the value attribute will not be generated. + * @param string $content the content enclosed within the button tag. It will NOT be HTML-encoded. + * Therefore you can pass in HTML code such as an image tag. If this is is coming from end users, + * you should consider [[encode()]] it to prevent XSS attacks. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated submit button tag + */ + public static function submitButton($name = null, $value = null, $content = 'Submit', $options = array()) + { + $options['type'] = 'submit'; + return static::button($name, $value, $content, $options); + } + + /** + * Generates a reset button tag. + * @param string $name the name attribute. If it is null, the name attribute will not be generated. + * @param string $value the value attribute. If it is null, the value attribute will not be generated. + * @param string $content the content enclosed within the button tag. It will NOT be HTML-encoded. + * Therefore you can pass in HTML code such as an image tag. If this is is coming from end users, + * you should consider [[encode()]] it to prevent XSS attacks. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated reset button tag + */ + public static function resetButton($name = null, $value = null, $content = 'Reset', $options = array()) + { + $options['type'] = 'reset'; + return static::button($name, $value, $content, $options); + } + + /** + * Generates an input type of the given type. + * @param string $type the type attribute. + * @param string $name the name attribute. If it is null, the name attribute will not be generated. + * @param string $value the value attribute. If it is null, the value attribute will not be generated. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated input tag + */ + public static function input($type, $name = null, $value = null, $options = array()) + { + $options['type'] = $type; + $options['name'] = $name; + $options['value'] = $value; + return static::tag('input', null, $options); + } + + /** + * Generates an input button. + * @param string $name the name attribute. + * @param string $value the value attribute. If it is null, the value attribute will not be generated. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated button tag + */ + public static function buttonInput($name, $value = 'Button', $options = array()) + { + return static::input('button', $name, $value, $options); + } + + /** + * Generates a submit input button. + * @param string $name the name attribute. If it is null, the name attribute will not be generated. + * @param string $value the value attribute. If it is null, the value attribute will not be generated. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated button tag + */ + public static function submitInput($name = null, $value = 'Submit', $options = array()) + { + return static::input('submit', $name, $value, $options); + } + + /** + * Generates a reset input button. + * @param string $name the name attribute. If it is null, the name attribute will not be generated. + * @param string $value the value attribute. If it is null, the value attribute will not be generated. + * @param array $options the attributes of the button tag. The values will be HTML-encoded using [[encode()]]. + * Attributes whose value is null will be ignored and not put in the tag returned. + * @return string the generated button tag + */ + public static function resetInput($name = null, $value = 'Reset', $options = array()) + { + return static::input('reset', $name, $value, $options); + } + + /** + * Generates a text input field. + * @param string $name the name attribute. + * @param string $value the value attribute. If it is null, the value attribute will not be generated. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated button tag + */ + public static function textInput($name, $value = null, $options = array()) + { + return static::input('text', $name, $value, $options); + } + + /** + * Generates a hidden input field. + * @param string $name the name attribute. + * @param string $value the value attribute. If it is null, the value attribute will not be generated. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated button tag + */ + public static function hiddenInput($name, $value = null, $options = array()) + { + return static::input('hidden', $name, $value, $options); + } + + /** + * Generates a password input field. + * @param string $name the name attribute. + * @param string $value the value attribute. If it is null, the value attribute will not be generated. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated button tag + */ + public static function passwordInput($name, $value = null, $options = array()) + { + return static::input('password', $name, $value, $options); + } + + /** + * Generates a file input field. + * To use a file input field, you should set the enclosing form's "enctype" attribute to + * be "multipart/form-data". After the form is submitted, the uploaded file information + * can be obtained via $_FILES[$name] (see PHP documentation). + * @param string $name the name attribute. + * @param string $value the value attribute. If it is null, the value attribute will not be generated. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated button tag + */ + public static function fileInput($name, $value = null, $options = array()) + { + return static::input('file', $name, $value, $options); + } + + /** + * Generates a text area input. + * @param string $name the input name + * @param string $value the input value. Note that it will be encoded using [[encode()]]. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated text area tag + */ + public static function textarea($name, $value = '', $options = array()) + { + $options['name'] = $name; + return static::tag('textarea', static::encode($value), $options); + } + + /** + * Generates a radio button input. + * @param string $name the name attribute. + * @param boolean $checked whether the radio button should be checked. + * @param string $value the value attribute. If it is null, the value attribute will not be rendered. + * @param array $options the tag options in terms of name-value pairs. The following options are supported: + * + * - uncheck: string, the value associated with the uncheck state of the radio button. When this attribute + * is present, a hidden input will be generated so that if the radio button is not checked and is submitted, + * the value of this attribute will still be submitted to the server via the hidden input. + * + * The rest of the options will be rendered as the attributes of the resulting tag. The values will + * be HTML-encoded using [[encode()]]. If a value is null, the corresponding attribute will not be rendered. + * + * @return string the generated radio button tag + */ + public static function radio($name, $checked = false, $value = '1', $options = array()) + { + $options['checked'] = $checked; + $options['value'] = $value; + if (isset($options['uncheck'])) { + // add a hidden field so that if the radio button is not selected, it still submits a value + $hidden = static::hiddenInput($name, $options['uncheck']); + unset($options['uncheck']); + } else { + $hidden = ''; + } + return $hidden . static::input('radio', $name, $value, $options); + } + + /** + * Generates a checkbox input. + * @param string $name the name attribute. + * @param boolean $checked whether the checkbox should be checked. + * @param string $value the value attribute. If it is null, the value attribute will not be rendered. + * @param array $options the tag options in terms of name-value pairs. The following options are supported: + * + * - uncheck: string, the value associated with the uncheck state of the checkbox. When this attribute + * is present, a hidden input will be generated so that if the checkbox is not checked and is submitted, + * the value of this attribute will still be submitted to the server via the hidden input. + * + * The rest of the options will be rendered as the attributes of the resulting tag. The values will + * be HTML-encoded using [[encode()]]. If a value is null, the corresponding attribute will not be rendered. + * + * @return string the generated checkbox tag + */ + public static function checkbox($name, $checked = false, $value = '1', $options = array()) + { + $options['checked'] = $checked; + $options['value'] = $value; + if (isset($options['uncheck'])) { + // add a hidden field so that if the checkbox is not selected, it still submits a value + $hidden = static::hiddenInput($name, $options['uncheck']); + unset($options['uncheck']); + } else { + $hidden = ''; + } + return $hidden . static::input('checkbox', $name, $value, $options); + } + + /** + * Generates a drop-down list. + * @param string $name the input name + * @param string $selection the selected value + * @param array $items the option data items. The array keys are option values, and the array values + * are the corresponding option labels. The array can also be nested (i.e. some array values are arrays too). + * For each sub-array, an option group will be generated whose label is the key associated with the sub-array. + * If you have a list of data models, you may convert them into the format described above using + * [[\yii\helpers\ArrayHelper::map()]]. + * + * Note, the values and labels will be automatically HTML-encoded by this method, and the blank spaces in + * the labels will also be HTML-encoded. + * @param array $options the tag options in terms of name-value pairs. The following options are supported: + * + * - prompt: string, a prompt text to be displayed as the first option; + * - options: array, the attributes for the select option tags. The array keys must be valid option values, + * and the array values are the extra attributes for the corresponding option tags. For example, + * + * ~~~ + * array( + * 'value1' => array('disabled' => true), + * 'value2' => array('label' => 'value 2'), + * ); + * ~~~ + * + * - groups: array, the attributes for the optgroup tags. The structure of this is similar to that of 'options', + * except that the array keys represent the optgroup labels specified in $items. + * + * The rest of the options will be rendered as the attributes of the resulting tag. The values will + * be HTML-encoded using [[encode()]]. If a value is null, the corresponding attribute will not be rendered. + * + * @return string the generated drop-down list tag + */ + public static function dropDownList($name, $selection = null, $items = array(), $options = array()) + { + $options['name'] = $name; + $selectOptions = static::renderSelectOptions($selection, $items, $options); + return static::tag('select', "\n" . $selectOptions . "\n", $options); + } + + /** + * Generates a list box. + * @param string $name the input name + * @param string|array $selection the selected value(s) + * @param array $items the option data items. The array keys are option values, and the array values + * are the corresponding option labels. The array can also be nested (i.e. some array values are arrays too). + * For each sub-array, an option group will be generated whose label is the key associated with the sub-array. + * If you have a list of data models, you may convert them into the format described above using + * [[\yii\helpers\ArrayHelper::map()]]. + * + * Note, the values and labels will be automatically HTML-encoded by this method, and the blank spaces in + * the labels will also be HTML-encoded. + * @param array $options the tag options in terms of name-value pairs. The following options are supported: + * + * - prompt: string, a prompt text to be displayed as the first option; + * - options: array, the attributes for the select option tags. The array keys must be valid option values, + * and the array values are the extra attributes for the corresponding option tags. For example, + * + * ~~~ + * array( + * 'value1' => array('disabled' => true), + * 'value2' => array('label' => 'value 2'), + * ); + * ~~~ + * + * - groups: array, the attributes for the optgroup tags. The structure of this is similar to that of 'options', + * except that the array keys represent the optgroup labels specified in $items. + * - unselect: string, the value that will be submitted when no option is selected. + * When this attribute is set, a hidden field will be generated so that if no option is selected in multiple + * mode, we can still obtain the posted unselect value. + * + * The rest of the options will be rendered as the attributes of the resulting tag. The values will + * be HTML-encoded using [[encode()]]. If a value is null, the corresponding attribute will not be rendered. + * + * @return string the generated list box tag + */ + public static function listBox($name, $selection = null, $items = array(), $options = array()) + { + if (!isset($options['size'])) { + $options['size'] = 4; + } + if (isset($options['multiple']) && $options['multiple'] && substr($name, -2) !== '[]') { + $name .= '[]'; + } + $options['name'] = $name; + if (isset($options['unselect'])) { + // add a hidden field so that if the list box has no option being selected, it still submits a value + if (substr($name, -2) === '[]') { + $name = substr($name, 0, -2); + } + $hidden = static::hiddenInput($name, $options['unselect']); + unset($options['unselect']); + } else { + $hidden = ''; + } + $selectOptions = static::renderSelectOptions($selection, $items, $options); + return $hidden . static::tag('select', "\n" . $selectOptions . "\n", $options); + } + + /** + * Generates a list of checkboxes. + * A checkbox list allows multiple selection, like [[listBox()]]. + * As a result, the corresponding submitted value is an array. + * @param string $name the name attribute of each checkbox. + * @param string|array $selection the selected value(s). + * @param array $items the data item used to generate the checkboxes. + * The array keys are the labels, while the array values are the corresponding checkbox values. + * Note that the labels will NOT be HTML-encoded, while the values will. + * @param array $options options (name => config) for the checkbox list. The following options are supported: + * + * - unselect: string, the value that should be submitted when none of the checkboxes is selected. + * By setting this option, a hidden input will be generated. + * - separator: string, the HTML code that separates items. + * - item: callable, a callback that can be used to customize the generation of the HTML code + * corresponding to a single item in $items. The signature of this callback must be: + * + * ~~~ + * function ($index, $label, $name, $checked, $value) + * ~~~ + * + * where $index is the zero-based index of the checkbox in the whole list; $label + * is the label for the checkbox; and $name, $value and $checked represent the name, + * value and the checked status of the checkbox input. + * @return string the generated checkbox list + */ + public static function checkboxList($name, $selection = null, $items = array(), $options = array()) + { + if (substr($name, -2) !== '[]') { + $name .= '[]'; + } + + $formatter = isset($options['item']) ? $options['item'] : null; + $lines = array(); + $index = 0; + foreach ($items as $value => $label) { + $checked = $selection !== null && + (!is_array($selection) && !strcmp($value, $selection) + || is_array($selection) && in_array($value, $selection)); + if ($formatter !== null) { + $lines[] = call_user_func($formatter, $index, $label, $name, $checked, $value); + } else { + $lines[] = static::label(static::checkbox($name, $checked, $value) . ' ' . $label); + } + $index++; + } + + if (isset($options['unselect'])) { + // add a hidden field so that if the list box has no option being selected, it still submits a value + $name2 = substr($name, -2) === '[]' ? substr($name, 0, -2) : $name; + $hidden = static::hiddenInput($name2, $options['unselect']); + } else { + $hidden = ''; + } + $separator = isset($options['separator']) ? $options['separator'] : "\n"; + + return $hidden . implode($separator, $lines); + } + + /** + * Generates a list of radio buttons. + * A radio button list is like a checkbox list, except that it only allows single selection. + * @param string $name the name attribute of each radio button. + * @param string|array $selection the selected value(s). + * @param array $items the data item used to generate the radio buttons. + * The array keys are the labels, while the array values are the corresponding radio button values. + * Note that the labels will NOT be HTML-encoded, while the values will. + * @param array $options options (name => config) for the radio button list. The following options are supported: + * + * - unselect: string, the value that should be submitted when none of the radio buttons is selected. + * By setting this option, a hidden input will be generated. + * - separator: string, the HTML code that separates items. + * - item: callable, a callback that can be used to customize the generation of the HTML code + * corresponding to a single item in $items. The signature of this callback must be: + * + * ~~~ + * function ($index, $label, $name, $checked, $value) + * ~~~ + * + * where $index is the zero-based index of the radio button in the whole list; $label + * is the label for the radio button; and $name, $value and $checked represent the name, + * value and the checked status of the radio button input. + * @return string the generated radio button list + */ + public static function radioList($name, $selection = null, $items = array(), $options = array()) + { + $formatter = isset($options['item']) ? $options['item'] : null; + $lines = array(); + $index = 0; + foreach ($items as $value => $label) { + $checked = $selection !== null && + (!is_array($selection) && !strcmp($value, $selection) + || is_array($selection) && in_array($value, $selection)); + if ($formatter !== null) { + $lines[] = call_user_func($formatter, $index, $label, $name, $checked, $value); + } else { + $lines[] = static::label(static::radio($name, $checked, $value) . ' ' . $label); + } + $index++; + } + + $separator = isset($options['separator']) ? $options['separator'] : "\n"; + if (isset($options['unselect'])) { + // add a hidden field so that if the list box has no option being selected, it still submits a value + $hidden = static::hiddenInput($name, $options['unselect']); + } else { + $hidden = ''; + } + + return $hidden . implode($separator, $lines); + } + + /** + * Renders the option tags that can be used by [[dropDownList()]] and [[listBox()]]. + * @param string|array $selection the selected value(s). This can be either a string for single selection + * or an array for multiple selections. + * @param array $items the option data items. The array keys are option values, and the array values + * are the corresponding option labels. The array can also be nested (i.e. some array values are arrays too). + * For each sub-array, an option group will be generated whose label is the key associated with the sub-array. + * If you have a list of data models, you may convert them into the format described above using + * [[\yii\helpers\ArrayHelper::map()]]. + * + * Note, the values and labels will be automatically HTML-encoded by this method, and the blank spaces in + * the labels will also be HTML-encoded. + * @param array $tagOptions the $options parameter that is passed to the [[dropDownList()]] or [[listBox()]] call. + * This method will take out these elements, if any: "prompt", "options" and "groups". See more details + * in [[dropDownList()]] for the explanation of these elements. + * + * @return string the generated list options + */ + public static function renderSelectOptions($selection, $items, &$tagOptions = array()) + { + $lines = array(); + if (isset($tagOptions['prompt'])) { + $prompt = str_replace(' ', ' ', static::encode($tagOptions['prompt'])); + $lines[] = static::tag('option', $prompt, array('value' => '')); + } + + $options = isset($tagOptions['options']) ? $tagOptions['options'] : array(); + $groups = isset($tagOptions['groups']) ? $tagOptions['groups'] : array(); + unset($tagOptions['prompt'], $tagOptions['options'], $tagOptions['groups']); + + foreach ($items as $key => $value) { + if (is_array($value)) { + $groupAttrs = isset($groups[$key]) ? $groups[$key] : array(); + $groupAttrs['label'] = $key; + $attrs = array('options' => $options, 'groups' => $groups); + $content = static::renderSelectOptions($selection, $value, $attrs); + $lines[] = static::tag('optgroup', "\n" . $content . "\n", $groupAttrs); + } else { + $attrs = isset($options[$key]) ? $options[$key] : array(); + $attrs['value'] = $key; + $attrs['selected'] = $selection !== null && + (!is_array($selection) && !strcmp($key, $selection) + || is_array($selection) && in_array($key, $selection)); + $lines[] = static::tag('option', str_replace(' ', ' ', static::encode($value)), $attrs); + } + } + + return implode("\n", $lines); + } + + /** + * Renders the HTML tag attributes. + * Boolean attributes such as s 'checked', 'disabled', 'readonly', will be handled specially + * according to [[booleanAttributes]] and [[showBooleanAttributeValues]]. + * @param array $attributes attributes to be rendered. The attribute values will be HTML-encoded using [[encode()]]. + * Attributes whose value is null will be ignored and not put in the rendering result. + * @return string the rendering result. If the attributes are not empty, they will be rendered + * into a string with a leading white space (such that it can be directly appended to the tag name + * in a tag. If there is no attribute, an empty string will be returned. + */ + public static function renderTagAttributes($attributes) + { + if (count($attributes) > 1) { + $sorted = array(); + foreach (static::$attributeOrder as $name) { + if (isset($attributes[$name])) { + $sorted[$name] = $attributes[$name]; + } + } + $attributes = array_merge($sorted, $attributes); + } + + $html = ''; + foreach ($attributes as $name => $value) { + if (isset(static::$booleanAttributes[strtolower($name)])) { + if ($value || strcasecmp($name, $value) === 0) { + $html .= static::$showBooleanAttributeValues ? " $name=\"$name\"" : " $name"; + } + } elseif ($value !== null) { + $html .= " $name=\"" . static::encode($value) . '"'; + } + } + return $html; + } + + /** + * Normalizes the input parameter to be a valid URL. + * + * If the input parameter + * + * - is an empty string: the currently requested URL will be returned; + * - is a non-empty string: it will be processed by [[Yii::getAlias()]] and returned; + * - is an array: the first array element is considered a route, while the rest of the name-value + * pairs are treated as the parameters to be used for URL creation using [[\yii\web\Controller::createUrl()]]. + * For example: `array('post/index', 'page' => 2)`, `array('index')`. + * + * @param array|string $url the parameter to be used to generate a valid URL + * @return string the normalized URL + * @throws InvalidParamException if the parameter is invalid. + */ + public static function url($url) + { + if (is_array($url)) { + if (isset($url[0])) { + $route = $url[0]; + $params = array_splice($url, 1); + if (Yii::$app->controller !== null) { + return Yii::$app->controller->createUrl($route, $params); + } else { + return Yii::$app->getUrlManager()->createUrl($route, $params); + } + } else { + throw new InvalidParamException('The array specifying a URL must contain at least one element.'); + } + } elseif ($url === '') { + return Yii::$app->getRequest()->getUrl(); + } else { + return Yii::getAlias($url); + } + } +} diff --git a/framework/helpers/base/SecurityHelper.php b/framework/helpers/base/SecurityHelper.php new file mode 100644 index 0000000..6ba48ba --- /dev/null +++ b/framework/helpers/base/SecurityHelper.php @@ -0,0 +1,272 @@ + + * @author Tom Worster + * @since 2.0 + */ +class SecurityHelper +{ + /** + * Encrypts data. + * @param string $data data to be encrypted. + * @param string $key the encryption secret key + * @return string the encrypted data + * @throws Exception if PHP Mcrypt extension is not loaded or failed to be initialized + * @see decrypt() + */ + public static function encrypt($data, $key) + { + $module = static::openCryptModule(); + $key = StringHelper::substr($key, 0, mcrypt_enc_get_key_size($module)); + srand(); + $iv = mcrypt_create_iv(mcrypt_enc_get_iv_size($module), MCRYPT_RAND); + mcrypt_generic_init($module, $key, $iv); + $encrypted = $iv . mcrypt_generic($module, $data); + mcrypt_generic_deinit($module); + mcrypt_module_close($module); + return $encrypted; + } + + /** + * Decrypts data + * @param string $data data to be decrypted. + * @param string $key the decryption secret key + * @return string the decrypted data + * @throws Exception if PHP Mcrypt extension is not loaded or failed to be initialized + * @see encrypt() + */ + public static function decrypt($data, $key) + { + $module = static::openCryptModule(); + $key = StringHelper::substr($key, 0, mcrypt_enc_get_key_size($module)); + $ivSize = mcrypt_enc_get_iv_size($module); + $iv = StringHelper::substr($data, 0, $ivSize); + mcrypt_generic_init($module, $key, $iv); + $decrypted = mdecrypt_generic($module, StringHelper::substr($data, $ivSize, StringHelper::strlen($data))); + mcrypt_generic_deinit($module); + mcrypt_module_close($module); + return rtrim($decrypted, "\0"); + } + + /** + * Prefixes data with a keyed hash value so that it can later be detected if it is tampered. + * @param string $data the data to be protected + * @param string $key the secret key to be used for generating hash + * @param string $algorithm the hashing algorithm (e.g. "md5", "sha1", "sha256", etc.). Call PHP "hash_algos()" + * function to see the supported hashing algorithms on your system. + * @return string the data prefixed with the keyed hash + * @see validateData() + * @see getSecretKey() + */ + public static function hashData($data, $key, $algorithm = 'sha256') + { + return hash_hmac($algorithm, $data, $key) . $data; + } + + /** + * Validates if the given data is tampered. + * @param string $data the data to be validated. The data must be previously + * generated by [[hashData()]]. + * @param string $key the secret key that was previously used to generate the hash for the data in [[hashData()]]. + * @param string $algorithm the hashing algorithm (e.g. "md5", "sha1", "sha256", etc.). Call PHP "hash_algos()" + * function to see the supported hashing algorithms on your system. This must be the same + * as the value passed to [[hashData()]] when generating the hash for the data. + * @return string the real data with the hash stripped off. False if the data is tampered. + * @see hashData() + */ + public static function validateData($data, $key, $algorithm = 'sha256') + { + $hashSize = StringHelper::strlen(hash_hmac($algorithm, 'test', $key)); + $n = StringHelper::strlen($data); + if ($n >= $hashSize) { + $hash = StringHelper::substr($data, 0, $hashSize); + $data2 = StringHelper::substr($data, $hashSize, $n - $hashSize); + return $hash === hash_hmac($algorithm, $data2, $key) ? $data2 : false; + } else { + return false; + } + } + + /** + * Returns a secret key associated with the specified name. + * If the secret key does not exist, a random key will be generated + * and saved in the file "keys.php" under the application's runtime directory + * so that the same secret key can be returned in future requests. + * @param string $name the name that is associated with the secret key + * @param integer $length the length of the key that should be generated if not exists + * @return string the secret key associated with the specified name + */ + public static function getSecretKey($name, $length = 32) + { + static $keys; + $keyFile = Yii::$app->getRuntimePath() . '/keys.php'; + if ($keys === null) { + $keys = is_file($keyFile) ? require($keyFile) : array(); + } + if (!isset($keys[$name])) { + // generate a 32-char random key + $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + $keys[$name] = substr(str_shuffle(str_repeat($chars, 5)), 0, $length); + file_put_contents($keyFile, " 30) { + throw new InvalidParamException('Hash is invalid.'); + } + + $test = crypt($password, $hash); + $n = strlen($test); + if (strlen($test) < 32 || $n !== strlen($hash)) { + return false; + } + + // Use a for-loop to compare two strings to prevent timing attacks. See: + // http://codereview.stackexchange.com/questions/13512 + $check = 0; + for ($i = 0; $i < $n; ++$i) { + $check |= (ord($test[$i]) ^ ord($hash[$i])); + } + + return $check === 0; + } + + /** + * Generates a salt that can be used to generate a password hash. + * + * The PHP [crypt()](http://php.net/manual/en/function.crypt.php) built-in function + * requires, for the Blowfish hash algorithm, a salt string in a specific format: + * "$2a$", "$2x$" or "$2y$", a two digit cost parameter, "$", and 22 characters + * from the alphabet "./0-9A-Za-z". + * + * @param integer $cost the cost parameter + * @return string the random salt value. + * @throws InvalidParamException if the cost parameter is not between 4 and 30 + */ + protected static function generateSalt($cost = 13) + { + $cost = (int)$cost; + if ($cost < 4 || $cost > 30) { + throw new InvalidParamException('Cost must be between 4 and 31.'); + } + + // Get 20 * 8bits of pseudo-random entropy from mt_rand(). + $rand = ''; + for ($i = 0; $i < 20; ++$i) { + $rand .= chr(mt_rand(0, 255)); + } + + // Add the microtime for a little more entropy. + $rand .= microtime(); + // Mix the bits cryptographically into a 20-byte binary string. + $rand = sha1($rand, true); + // Form the prefix that specifies Blowfish algorithm and cost parameter. + $salt = sprintf("$2y$%02d$", $cost); + // Append the random salt data in the required base64 format. + $salt .= str_replace('+', '.', substr(base64_encode($rand), 0, 22)); + return $salt; + } +} \ No newline at end of file diff --git a/framework/helpers/base/StringHelper.php b/framework/helpers/base/StringHelper.php new file mode 100644 index 0000000..cb4b09b --- /dev/null +++ b/framework/helpers/base/StringHelper.php @@ -0,0 +1,125 @@ + + * @author Alex Makarov + * @since 2.0 + */ +class StringHelper +{ + /** + * Returns the number of bytes in the given string. + * This method ensures the string is treated as a byte array. + * It will use `mb_strlen()` if it is available. + * @param string $string the string being measured for length + * @return integer the number of bytes in the given string. + */ + public static function strlen($string) + { + return function_exists('mb_strlen') ? mb_strlen($string, '8bit') : strlen($string); + } + + /** + * Returns the portion of string specified by the start and length parameters. + * This method ensures the string is treated as a byte array. + * It will use `mb_substr()` if it is available. + * @param string $string the input string. Must be one character or longer. + * @param integer $start the starting position + * @param integer $length the desired portion length + * @return string the extracted part of string, or FALSE on failure or an empty string. + * @see http://www.php.net/manual/en/function.substr.php + */ + public static function substr($string, $start, $length) + { + return function_exists('mb_substr') ? mb_substr($string, $start, $length, '8bit') : substr($string, $start, $length); + } + + /** + * Converts a word to its plural form. + * Note that this is for English only! + * For example, 'apple' will become 'apples', and 'child' will become 'children'. + * @param string $name the word to be pluralized + * @return string the pluralized word + */ + public static function pluralize($name) + { + static $rules = array( + '/(m)ove$/i' => '\1oves', + '/(f)oot$/i' => '\1eet', + '/(c)hild$/i' => '\1hildren', + '/(h)uman$/i' => '\1umans', + '/(m)an$/i' => '\1en', + '/(s)taff$/i' => '\1taff', + '/(t)ooth$/i' => '\1eeth', + '/(p)erson$/i' => '\1eople', + '/([m|l])ouse$/i' => '\1ice', + '/(x|ch|ss|sh|us|as|is|os)$/i' => '\1es', + '/([^aeiouy]|qu)y$/i' => '\1ies', + '/(?:([^f])fe|([lr])f)$/i' => '\1\2ves', + '/(shea|lea|loa|thie)f$/i' => '\1ves', + '/([ti])um$/i' => '\1a', + '/(tomat|potat|ech|her|vet)o$/i' => '\1oes', + '/(bu)s$/i' => '\1ses', + '/(ax|test)is$/i' => '\1es', + '/s$/' => 's', + ); + foreach ($rules as $rule => $replacement) { + if (preg_match($rule, $name)) { + return preg_replace($rule, $replacement, $name); + } + } + return $name . 's'; + } + + /** + * Converts a CamelCase name into space-separated words. + * For example, 'PostTag' will be converted to 'Post Tag'. + * @param string $name the string to be converted + * @param boolean $ucwords whether to capitalize the first letter in each word + * @return string the resulting words + */ + public static function camel2words($name, $ucwords = true) + { + $label = trim(strtolower(str_replace(array('-', '_', '.'), ' ', preg_replace('/(? + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008-2011 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\helpers\base; + +/** + * VarDumper is intended to replace the buggy PHP function var_dump and print_r. + * It can correctly identify the recursively referenced objects in a complex + * object structure. It also has a recursive depth control to avoid indefinite + * recursive display of some peculiar variables. + * + * VarDumper can be used as follows, + * + * ~~~ + * VarDumper::dump($var); + * ~~~ + * + * @author Qiang Xue + * @since 2.0 + */ +class VarDumper +{ + private static $_objects; + private static $_output; + private static $_depth; + + /** + * Displays a variable. + * This method achieves the similar functionality as var_dump and print_r + * but is more robust when handling complex objects such as Yii controllers. + * @param mixed $var variable to be dumped + * @param integer $depth maximum depth that the dumper should go into the variable. Defaults to 10. + * @param boolean $highlight whether the result should be syntax-highlighted + */ + public static function dump($var, $depth = 10, $highlight = false) + { + echo self::dumpAsString($var, $depth, $highlight); + } + + /** + * Dumps a variable in terms of a string. + * This method achieves the similar functionality as var_dump and print_r + * but is more robust when handling complex objects such as Yii controllers. + * @param mixed $var variable to be dumped + * @param integer $depth maximum depth that the dumper should go into the variable. Defaults to 10. + * @param boolean $highlight whether the result should be syntax-highlighted + * @return string the string representation of the variable + */ + public static function dumpAsString($var, $depth = 10, $highlight = false) + { + self::$_output = ''; + self::$_objects = array(); + self::$_depth = $depth; + self::dumpInternal($var, 0); + if ($highlight) { + $result = highlight_string("/', '', $result, 1); + } + return self::$_output; + } + + /* + * @param mixed $var variable to be dumped + * @param integer $level depth level + */ + private static function dumpInternal($var, $level) + { + switch (gettype($var)) { + case 'boolean': + self::$_output .= $var ? 'true' : 'false'; + break; + case 'integer': + self::$_output .= "$var"; + break; + case 'double': + self::$_output .= "$var"; + break; + case 'string': + self::$_output .= "'" . addslashes($var) . "'"; + break; + case 'resource': + self::$_output .= '{resource}'; + break; + case 'NULL': + self::$_output .= "null"; + break; + case 'unknown type': + self::$_output .= '{unknown}'; + break; + case 'array': + if (self::$_depth <= $level) { + self::$_output .= 'array(...)'; + } elseif (empty($var)) { + self::$_output .= 'array()'; + } else { + $keys = array_keys($var); + $spaces = str_repeat(' ', $level * 4); + self::$_output .= "array\n" . $spaces . '('; + foreach ($keys as $key) { + self::$_output .= "\n" . $spaces . ' '; + self::dumpInternal($key, 0); + self::$_output .= ' => '; + self::dumpInternal($var[$key], $level + 1); + } + self::$_output .= "\n" . $spaces . ')'; + } + break; + case 'object': + if (($id = array_search($var, self::$_objects, true)) !== false) { + self::$_output .= get_class($var) . '#' . ($id + 1) . '(...)'; + } elseif (self::$_depth <= $level) { + self::$_output .= get_class($var) . '(...)'; + } else { + $id = self::$_objects[] = $var; + $className = get_class($var); + $members = (array)$var; + $spaces = str_repeat(' ', $level * 4); + self::$_output .= "$className#$id\n" . $spaces . '('; + foreach ($members as $key => $value) { + $keyDisplay = strtr(trim($key), array("\0" => ':')); + self::$_output .= "\n" . $spaces . " [$keyDisplay] => "; + self::dumpInternal($value, $level + 1); + } + self::$_output .= "\n" . $spaces . ')'; + } + break; + } + } +} \ No newline at end of file diff --git a/framework/helpers/base/mimeTypes.php b/framework/helpers/base/mimeTypes.php new file mode 100644 index 0000000..ffdba4b --- /dev/null +++ b/framework/helpers/base/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/helpers/mimeTypes.php b/framework/helpers/mimeTypes.php deleted file mode 100644 index ffdba4b..0000000 --- a/framework/helpers/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 f52bc48576968e2ba407061a8169ad958677f7cb Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Sat, 13 Apr 2013 23:16:11 -0400 Subject: [PATCH 059/104] use error_log to log fatal errors. --- framework/base/Application.php | 137 ++++++++++++++++------------------------- 1 file changed, 54 insertions(+), 83 deletions(-) diff --git a/framework/base/Application.php b/framework/base/Application.php index 1053210..b9f01d7 100644 --- a/framework/base/Application.php +++ b/framework/base/Application.php @@ -13,36 +13,6 @@ use yii\helpers\FileHelper; /** * Application is the base class for all application classes. * - * An application serves as the global context that the user request - * is being processed. It manages a set of application components that - * provide specific functionalities to the whole application. - * - * The core application components provided by Application are the following: - *
      - *
    • {@link getErrorHandler errorHandler}: handles PHP errors and - * uncaught exceptions. This application component is dynamically loaded when needed.
    • - *
    • {@link getSecurityManager securityManager}: provides security-related - * services, such as hashing, encryption. This application component is dynamically - * loaded when needed.
    • - *
    • {@link getStatePersister statePersister}: provides global state - * persistence method. This application component is dynamically loaded when needed.
    • - *
    • {@link getCache cache}: provides caching feature. This application component is - * disabled by default.
    • - *
    - * - * Application will undergo the following life cycles when processing a user request: - *
      - *
    1. load application configuration;
    2. - *
    3. set up class autoloader and error handling;
    4. - *
    5. load static application components;
    6. - *
    7. {@link beforeRequest}: preprocess the user request; `beforeRequest` event raised.
    8. - *
    9. {@link processRequest}: process the user request;
    10. - *
    11. {@link afterRequest}: postprocess the user request; `afterRequest` event raised.
    12. - *
    - * - * Starting from lifecycle 3, if a PHP error or an uncaught exception occurs, - * the application will switch to its error handling logic and jump to step 6 afterwards. - * * @author Qiang Xue * @since 2.0 */ @@ -157,30 +127,6 @@ class Application extends Module } /** - * Handles fatal PHP errors - */ - public function handleFatalError() - { - if (YII_ENABLE_ERROR_HANDLER) { - $error = error_get_last(); - - if (ErrorException::isFatalError($error)) { - unset($this->_memoryReserve); - $exception = new ErrorException($error['message'], $error['type'], $error['type'], $error['file'], $error['line']); - $this->logException($exception); - - if (($handler = $this->getErrorHandler()) !== null) { - @$handler->handle($exception); - } else { - $this->renderException($exception); - } - - exit(1); - } - } - } - - /** * Runs the application. * This is the main entrance of an application. * @return integer the exit status (0 means normal, non-zero values mean abnormal) @@ -384,6 +330,45 @@ class Application extends Module } /** + * Handles uncaught PHP exceptions. + * + * This method is implemented as a PHP exception handler. It requires + * that constant YII_ENABLE_ERROR_HANDLER be defined true. + * + * @param \Exception $exception exception that is not caught + */ + public function handleException($exception) + { + // disable error capturing to avoid recursive errors while handling exceptions + restore_error_handler(); + restore_exception_handler(); + + try { + $this->logException($exception); + + if (($handler = $this->getErrorHandler()) !== null) { + $handler->handle($exception); + } else { + $this->renderException($exception); + } + + $this->end(1); + + } catch (\Exception $e) { + // exception could be thrown in end() or ErrorHandler::handle() + $msg = (string)$e; + $msg .= "\nPrevious exception:\n"; + $msg .= (string)$exception; + if (YII_DEBUG) { + echo $msg; + } + $msg .= "\n\$_SERVER = " . var_export($_SERVER, true); + error_log($msg); + exit(1); + } + } + + /** * Handles PHP execution errors such as warnings, notices. * * This method is used as a PHP error handler. It will simply raise an `ErrorException`. @@ -414,41 +399,27 @@ class Application extends Module } /** - * Handles uncaught PHP exceptions. - * - * This method is implemented as a PHP exception handler. It requires - * that constant YII_ENABLE_ERROR_HANDLER be defined true. - * - * @param \Exception $exception exception that is not caught + * Handles fatal PHP errors */ - public function handleException($exception) + public function handleFatalError() { - // disable error capturing to avoid recursive errors while handling exceptions - restore_error_handler(); - restore_exception_handler(); - - try { - $this->logException($exception); + if (YII_ENABLE_ERROR_HANDLER) { + $error = error_get_last(); - if (($handler = $this->getErrorHandler()) !== null) { - $handler->handle($exception); - } else { - $this->renderException($exception); - } + if (ErrorException::isFatalError($error)) { + unset($this->_memoryReserve); + $exception = new ErrorException($error['message'], $error['type'], $error['type'], $error['file'], $error['line']); + // use error_log because it's too late to use Yii log + error_log($exception); - $this->end(1); + if (($handler = $this->getErrorHandler()) !== null) { + @$handler->handle($exception); + } else { + $this->renderException($exception); + } - } catch (\Exception $e) { - // exception could be thrown in end() or ErrorHandler::handle() - $msg = (string)$e; - $msg .= "\nPrevious exception:\n"; - $msg .= (string)$exception; - if (YII_DEBUG) { - echo $msg; + exit(1); } - $msg .= "\n\$_SERVER = " . var_export($_SERVER, true); - error_log($msg); - exit(1); } } From 34c0794f0097588d50a032818964b027751ce038 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Sun, 14 Apr 2013 22:50:35 -0400 Subject: [PATCH 060/104] script WIP --- framework/base/View.php | 9 +-- framework/base/ViewContent.php | 176 +++++++++++++++++++++++++++++++++++++++-- 2 files changed, 173 insertions(+), 12 deletions(-) diff --git a/framework/base/View.php b/framework/base/View.php index d1a3c5f..247dfd2 100644 --- a/framework/base/View.php +++ b/framework/base/View.php @@ -37,7 +37,7 @@ class View extends Component /** * @var ViewContent */ - public $content; + public $page; /** * @var mixed custom parameters that are shared among view templates. */ @@ -88,10 +88,10 @@ class View extends Component if (is_array($this->theme)) { $this->theme = Yii::createObject($this->theme); } - if (is_array($this->content)) { - $this->content = Yii::createObject($this->content); + if (is_array($this->page)) { + $this->page = Yii::createObject($this->page); } else { - $this->content = new ViewContent; + $this->page = new ViewContent; } } @@ -166,7 +166,6 @@ class View extends Component } else { $output = $this->renderPhpFile($viewFile, $params); } - $output = $this->content->populate($output); $this->afterRender($viewFile, $output); } diff --git a/framework/base/ViewContent.php b/framework/base/ViewContent.php index cea3c7c..9bdeaef 100644 --- a/framework/base/ViewContent.php +++ b/framework/base/ViewContent.php @@ -8,6 +8,7 @@ namespace yii\base; use Yii; +use yii\helpers\Html; /** * @author Qiang Xue @@ -19,6 +20,10 @@ class ViewContent extends Component const POS_BEGIN = 2; const POS_END = 3; + const TOKEN_HEAD = ''; + const TOKEN_BODY_BEGIN = ''; + const TOKEN_BODY_END = ''; + /** * @var array * @@ -44,7 +49,7 @@ class ViewContent extends Component * ) * ~~~ */ - public $bundles; + public $assetBundles; public $title; public $metaTags; public $linkTags; @@ -57,11 +62,6 @@ class ViewContent extends Component public $jsInBody; public $jsFilesInBody; - public function populate($content) - { - return $content; - } - public function reset() { $this->title = null; @@ -77,7 +77,169 @@ class ViewContent extends Component $this->jsFilesInBody = null; } - public function renderScripts($pos) + public function begin() + { + ob_start(); + ob_implicit_flush(false); + } + + public function end() + { + $content = ob_get_clean(); + echo $this->populate($content); + } + + public function beginBody() + { + echo self::TOKEN_BODY_BEGIN; + } + + public function endBody() + { + echo self::TOKEN_BODY_END; + } + + public function head() + { + echo self::TOKEN_HEAD; + } + + public function requireAssetBundle($name) + { + if (!isset($this->assetBundles[$name])) { + $bundle = Yii::$app->assets->getBundle($name); + if ($bundle !== null) { + $this->assetBundles[$name] = $bundle; + } else { + throw new InvalidConfigException("Unknown asset bundle: $name"); + } + foreach ($bundle->depends as $d) { + $this->requireAssetBundle($d); + } + } + } + + public function registerMetaTag($options, $key = null) + { + if ($key === null) { + $this->metaTags[] = Html::tag('meta', '', $options); + } else { + $this->metaTags[$key] = Html::tag('meta', '', $options); + } + } + + public function registerLinkTag($options, $key = null) + { + if ($key === null) { + $this->linkTags[] = Html::tag('link', '', $options); + } else { + $this->linkTags[$key] = Html::tag('link', '', $options); + } + } + + public function registerCss($css, $options = array(), $key = null) + { + $key = $key ?: $css; + $this->css[$key] = Html::style($css, $options); + } + + public function registerCssFile($url, $options = array(), $key = null) + { + $key = $key ?: $url; + $this->cssFiles[$key] = Html::cssFile($url, $options); + } + + public function registerJs($js, $position = self::POS_END, $options = array(), $key = null) + { + $key = $key ?: $js; + $html = Html::script($js, $options); + if ($position == self::POS_END) { + $this->js[$key] = $html; + } elseif ($position == self::POS_HEAD) { + $this->jsInHead[$key] = $html; + } elseif ($position == self::POS_BEGIN) { + $this->jsInBody[$key] = $html; + } else { + throw new InvalidParamException("Unknown position: $position"); + } + } + + public function registerJsFile($url, $position = self::POS_END, $options = array(), $key = null) + { + $key = $key ?: $url; + $html = Html::jsFile($url, $options); + if ($position == self::POS_END) { + $this->jsFiles[$key] = $html; + } elseif ($position == self::POS_HEAD) { + $this->jsFilesInHead[$key] = $html; + } elseif ($position == self::POS_BEGIN) { + $this->jsFilesInBody[$key] = $html; + } else { + throw new InvalidParamException("Unknown position: $position"); + } + } + + + protected function populate($content) + { + $this->expandAssetBundles(); + return strtr($content, array( + self::TOKEN_HEAD => $this->getHeadHtml(), + self::TOKEN_BODY_BEGIN => $this->getBodyBeginHtml(), + self::TOKEN_BODY_END => $this->getBodyEndHtml(), + )); + } + + protected function expandAssetBundles() + { + + } + + protected function getHeadHtml() + { + $lines = array(); + if (!empty($this->metaTags)) { + $lines[] = implode("\n", $this->cssFiles); + } + if (!empty($this->linkTags)) { + $lines[] = implode("\n", $this->cssFiles); + } + if (!empty($this->cssFiles)) { + $lines[] = implode("\n", $this->cssFiles); + } + if (!empty($this->css)) { + $lines[] = implode("\n", $this->css); + } + if (!empty($this->jsFilesInHead)) { + $lines[] = implode("\n", $this->jsFilesInHead); + } + if (!empty($this->jsInHead)) { + $lines[] = implode("\n", $this->jsInHead); + } + return implode("\n", $lines); + } + + protected function getBodyBeginHtml() + { + $lines = array(); + if (!empty($this->jsFilesInBody)) { + $lines[] = implode("\n", $this->jsFilesInBody); + } + if (!empty($this->jsInHead)) { + $lines[] = implode("\n", $this->jsInBody); + } + return implode("\n", $lines); + } + + protected function getBodyEndHtml() { + $lines = array(); + if (!empty($this->jsFiles)) { + $lines[] = implode("\n", $this->jsFiles); + } + if (!empty($this->js)) { + $lines[] = implode("\n", $this->js); + } + return implode("\n", $lines); } } \ No newline at end of file From c49222f79961c5aa279d11383b53263ec46fcee4 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Mon, 15 Apr 2013 15:57:34 -0400 Subject: [PATCH 061/104] script WIP --- framework/base/View.php | 2 +- framework/base/ViewContent.php | 63 ++++----- framework/web/AssetBundle.php | 68 ++++++++++ framework/web/AssetManager.php | 288 +++++++++++++++++++---------------------- 4 files changed, 226 insertions(+), 195 deletions(-) create mode 100644 framework/web/AssetBundle.php diff --git a/framework/base/View.php b/framework/base/View.php index 247dfd2..a794e08 100644 --- a/framework/base/View.php +++ b/framework/base/View.php @@ -241,7 +241,7 @@ class View extends Component { if (!empty($this->cacheStack)) { $n = count($this->dynamicPlaceholders); - $placeholder = ""; + $placeholder = ""; $this->addDynamicPlaceholder($placeholder, $statements); return $placeholder; } else { diff --git a/framework/base/ViewContent.php b/framework/base/ViewContent.php index 9bdeaef..797dba2 100644 --- a/framework/base/ViewContent.php +++ b/framework/base/ViewContent.php @@ -25,30 +25,9 @@ class ViewContent extends Component const TOKEN_BODY_END = ''; /** - * @var array - * - * Each asset bundle should be declared with the following structure: - * - * ~~~ - * array( - * 'basePath' => '...', - * 'baseUrl' => '...', // if missing, the bundle will be published to the "www/assets" folder - * 'js' => array( - * 'js/main.js', - * 'js/menu.js', - * 'js/base.js' => self::POS_HEAD, - * 'css' => array( - * 'css/main.css', - * 'css/menu.css', - * ), - * 'depends' => array( - * 'jquery', - * 'yii', - * 'yii/treeview', - * ), - * ) - * ~~~ + * @var \yii\web\AssetManager */ + public $assetManager; public $assetBundles; public $title; public $metaTags; @@ -62,6 +41,14 @@ class ViewContent extends Component public $jsInBody; public $jsFilesInBody; + public function init() + { + parent::init(); + if ($this->assetManager === null) { + $this->assetManager = Yii::$app->getAssetManager(); + } + } + public function reset() { $this->title = null; @@ -104,21 +91,22 @@ class ViewContent extends Component echo self::TOKEN_HEAD; } - public function requireAssetBundle($name) + public function registerAssetBundle($name) { if (!isset($this->assetBundles[$name])) { - $bundle = Yii::$app->assets->getBundle($name); + $bundle = $this->assetManager->getBundle($name); if ($bundle !== null) { - $this->assetBundles[$name] = $bundle; + $this->assetBundles[$name] = false; + $bundle->registerWith($this); + $this->assetBundles[$name] = true; } else { throw new InvalidConfigException("Unknown asset bundle: $name"); } - foreach ($bundle->depends as $d) { - $this->requireAssetBundle($d); - } + } elseif ($this->assetBundles[$name] === false) { + throw new InvalidConfigException("A cyclic dependency is detected for bundle '$name'."); } } - + public function registerMetaTag($options, $key = null) { if ($key === null) { @@ -149,8 +137,10 @@ class ViewContent extends Component $this->cssFiles[$key] = Html::cssFile($url, $options); } - public function registerJs($js, $position = self::POS_END, $options = array(), $key = null) + public function registerJs($js, $options = array(), $key = null) { + $position = isset($options['position']) ? $options['position'] : self::POS_END; + unset($options['position']); $key = $key ?: $js; $html = Html::script($js, $options); if ($position == self::POS_END) { @@ -164,8 +154,10 @@ class ViewContent extends Component } } - public function registerJsFile($url, $position = self::POS_END, $options = array(), $key = null) + public function registerJsFile($url, $options = array(), $key = null) { + $position = isset($options['position']) ? $options['position'] : self::POS_END; + unset($options['position']); $key = $key ?: $url; $html = Html::jsFile($url, $options); if ($position == self::POS_END) { @@ -178,11 +170,9 @@ class ViewContent extends Component throw new InvalidParamException("Unknown position: $position"); } } - protected function populate($content) { - $this->expandAssetBundles(); return strtr($content, array( self::TOKEN_HEAD => $this->getHeadHtml(), self::TOKEN_BODY_BEGIN => $this->getBodyBeginHtml(), @@ -190,11 +180,6 @@ class ViewContent extends Component )); } - protected function expandAssetBundles() - { - - } - protected function getHeadHtml() { $lines = array(); diff --git a/framework/web/AssetBundle.php b/framework/web/AssetBundle.php new file mode 100644 index 0000000..3e4541e --- /dev/null +++ b/framework/web/AssetBundle.php @@ -0,0 +1,68 @@ + '...', + * 'baseUrl' => '...', // if missing, the bundle will be published to the "www/assets" folder + * 'js' => array( + * 'js/main.js', + * 'js/menu.js', + * 'js/base.js' => self::POS_HEAD, + * 'css' => array( + * 'css/main.css', + * 'css/menu.css', + * ), + * 'depends' => array( + * 'jquery', + * 'yii', + * 'yii/treeview', + * ), + * ) + * ~~~ + * @author Qiang Xue + * @since 2.0 + */ +class AssetBundle extends Object +{ + public $basePath; + public $baseUrl; // if missing, the bundle will be published to the "www/assets" folder + public $js = array(); + public $css = array(); + public $depends = array(); + + /** + * @param \yii\base\ViewContent $content + */ + public function registerWith($content) + { + foreach ($this->depends as $name) { + $content->registerAssetBundle($name); + } + foreach ($this->js as $js => $options) { + if (is_array($options)) { + $content->registerJsFile($js, $options); + } else { + $content->registerJsFile($options); + } + } + foreach ($this->css as $css => $options) { + if (is_array($options)) { + $content->registerCssFile($css, $options); + } else { + $content->registerCssFile($options); + } + } + } +} \ No newline at end of file diff --git a/framework/web/AssetManager.php b/framework/web/AssetManager.php index 60f4c07..3cb1e83 100644 --- a/framework/web/AssetManager.php +++ b/framework/web/AssetManager.php @@ -1,140 +1,121 @@ * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008-2011 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ +namespace yii\web; + +use Yii; +use yii\base\Component; +use yii\base\InvalidConfigException; +use yii\base\InvalidParamException; /** - * CAssetManager is a Web application component that manages private files (called assets) and makes them accessible by Web clients. - * - * It achieves this goal by copying assets to a Web-accessible directory - * and returns the corresponding URL for accessing them. - * - * To publish an asset, simply call {@link publish()}. - * - * The Web-accessible directory holding the published files is specified - * by {@link setBasePath basePath}, which defaults to the "assets" directory - * under the directory containing the application entry script file. - * The property {@link setBaseUrl baseUrl} refers to the URL for accessing - * the {@link setBasePath basePath}. - * - * @property string $basePath The root directory storing the published asset files. Defaults to 'WebRoot/assets'. - * @property string $baseUrl The base url that the published asset files can be accessed. - * Note, the ending slashes are stripped off. Defaults to '/AppBaseUrl/assets'. * * @author Qiang Xue - * @version $Id$ - * @package system.web - * @since 1.0 + * @since 2.0 */ -class CAssetManager extends CApplicationComponent +class AssetManager extends Component { /** - * Default web accessible base path for storing private files + * @var array list of asset bundles. The keys are the bundle names, and the values are the configuration + * arrays for creating [[AssetBundle]] objects. Besides the bundles listed here, the asset manager + * may look for bundles declared in extensions. For more details, please refer to [[getBundle()]]. + */ + public $bundles; + /** + * @var + */ + public $bundleMap; + /** + * @return string the root directory storing the published asset files. + */ + public $basePath = '@wwwroot/assets'; + /** + * @return string the base URL through which the published asset files can be accessed. */ - const DEFAULT_BASEPATH='assets'; + public $baseUrl = '@www/assets'; /** * @var boolean whether to use symbolic link to publish asset files. Defaults to false, meaning - * asset files are copied to public folders. Using symbolic links has the benefit that the published - * assets will always be consistent with the source assets. This is especially useful during development. + * asset files are copied to [[basePath]]. Using symbolic links has the benefit that the published + * assets will always be consistent with the source assets and there is no copy operation required. + * This is especially useful during development. * * However, there are special requirements for hosting environments in order to use symbolic links. * In particular, symbolic links are supported only on Linux/Unix, and Windows Vista/2008 or greater. - * The latter requires PHP 5.3 or greater. * * Moreover, some Web servers need to be properly configured so that the linked assets are accessible * to Web users. For example, for Apache Web server, the following configuration directive should be added * for the Web folder: - *
    -	 * Options FollowSymLinks
    -	 * 
    * - * @since 1.1.5 + * ~~~ + * Options FollowSymLinks + * ~~~ */ - public $linkAssets=false; + public $linkAssets = false; /** * @var array list of directories and files which should be excluded from the publishing process. * Defaults to exclude '.svn' and '.gitignore' files only. This option has no effect if {@link linkAssets} is enabled. * @since 1.1.6 **/ - public $excludeFiles=array('.svn','.gitignore'); + public $excludeFiles = array('.svn', '.gitignore'); /** * @var integer the permission to be set for newly generated asset files. * This value will be used by PHP chmod function. * Defaults to 0666, meaning the file is read-writable by all users. * @since 1.1.8 */ - public $newFileMode=0666; + public $newFileMode = 0666; /** * @var integer the permission to be set for newly generated asset directories. * This value will be used by PHP chmod function. * Defaults to 0777, meaning the directory can be read, written and executed by all users. * @since 1.1.8 */ - public $newDirMode=0777; - /** - * @var string base web accessible path for storing private files - */ - private $_basePath; - /** - * @var string base URL for accessing the publishing directory. - */ - private $_baseUrl; + public $newDirMode = 0777; /** * @var array published assets */ - private $_published=array(); + private $_published = array(); - /** - * @return string the root directory storing the published asset files. Defaults to 'WebRoot/assets'. - */ - public function getBasePath() + public function init() { - if($this->_basePath===null) - { - $request=\Yii::$app->getRequest(); - $this->setBasePath(dirname($request->getScriptFile()).DIRECTORY_SEPARATOR.self::DEFAULT_BASEPATH); + parent::init(); + $this->basePath = Yii::getAlias($this->basePath); + if (!is_dir($this->basePath)) { + throw new InvalidConfigException("The directory does not exist: {$this->basePath}"); + } elseif (!is_writable($this->basePath)) { + throw new InvalidConfigException("The directory is not writable by the Web process: {$this->basePath}"); + } else { + $this->base = realpath($this->basePath); } - return $this->_basePath; - } - - /** - * Sets the root directory storing published asset files. - * @param string $value the root directory storing published asset files - * @throws CException if the base path is invalid - */ - public function setBasePath($value) - { - if(($basePath=realpath($value))!==false && is_dir($basePath) && is_writable($basePath)) - $this->_basePath=$basePath; - else - throw new CException(Yii::t('yii|CAssetManager.basePath "{path}" is invalid. Please make sure the directory exists and is writable by the Web server process.', - array('{path}'=>$value))); + $this->baseUrl = rtrim(Yii::getAlias($this->getBaseUrl), '/'); } /** - * @return string the base url that the published asset files can be accessed. - * Note, the ending slashes are stripped off. Defaults to '/AppBaseUrl/assets'. + * @param string $name + * @return AssetBundle + * @throws InvalidParamException */ - public function getBaseUrl() + public function getBundle($name) { - if($this->_baseUrl===null) - { - $request=\Yii::$app->getRequest(); - $this->setBaseUrl($request->getBaseUrl().'/'.self::DEFAULT_BASEPATH); + if (!isset($this->bundles[$name])) { + $manifest = Yii::getAlias("@{$name}/assets.php", false); + if ($manifest === false) { + throw new InvalidParamException("Unable to find the asset bundle: $name"); + } + $this->bundles[$name] = require($manifest); } - return $this->_baseUrl; - } - - /** - * @param string $value the base url that the published asset files can be accessed - */ - public function setBaseUrl($value) - { - $this->_baseUrl=rtrim($value,'/'); + if (is_array($this->bundles[$name])) { + $config = $this->bundles[$name]; + if (!isset($config['class'])) { + $config['class'] = 'yii\\web\\AssetBundle'; + $this->bundles[$name] = Yii::createObject($config); + } + } + return $this->bundles[$name]; } /** @@ -173,69 +154,65 @@ class CAssetManager extends CApplicationComponent * @return string an absolute URL to the published asset * @throws CException if the asset to be published does not exist. */ - public function publish($path,$hashByName=false,$level=-1,$forceCopy=false) + public function publish($path, $hashByName = false, $level = -1, $forceCopy = false) { - if(isset($this->_published[$path])) + if (isset($this->_published[$path])) { return $this->_published[$path]; - else if(($src=realpath($path))!==false) - { - if(is_file($src)) - { - $dir=$this->hash($hashByName ? basename($src) : dirname($src).filemtime($src)); - $fileName=basename($src); - $dstDir=$this->getBasePath().DIRECTORY_SEPARATOR.$dir; - $dstFile=$dstDir.DIRECTORY_SEPARATOR.$fileName; + } else { + if (($src = realpath($path)) !== false) { + if (is_file($src)) { + $dir = $this->hash($hashByName ? basename($src) : dirname($src) . filemtime($src)); + $fileName = basename($src); + $dstDir = $this->getBasePath() . DIRECTORY_SEPARATOR . $dir; + $dstFile = $dstDir . DIRECTORY_SEPARATOR . $fileName; - if($this->linkAssets) - { - if(!is_file($dstFile)) - { - if(!is_dir($dstDir)) - { - mkdir($dstDir); - @chmod($dstDir, $this->newDirMode); + if ($this->linkAssets) { + if (!is_file($dstFile)) { + if (!is_dir($dstDir)) { + mkdir($dstDir); + @chmod($dstDir, $this->newDirMode); + } + symlink($src, $dstFile); + } + } else { + if (@filemtime($dstFile) < @filemtime($src)) { + if (!is_dir($dstDir)) { + mkdir($dstDir); + @chmod($dstDir, $this->newDirMode); + } + copy($src, $dstFile); + @chmod($dstFile, $this->newFileMode); } - symlink($src,$dstFile); - } - } - else if(@filemtime($dstFile)<@filemtime($src)) - { - if(!is_dir($dstDir)) - { - mkdir($dstDir); - @chmod($dstDir, $this->newDirMode); } - copy($src,$dstFile); - @chmod($dstFile, $this->newFileMode); - } - return $this->_published[$path]=$this->getBaseUrl()."/$dir/$fileName"; - } - else if(is_dir($src)) - { - $dir=$this->hash($hashByName ? basename($src) : $src.filemtime($src)); - $dstDir=$this->getBasePath().DIRECTORY_SEPARATOR.$dir; + return $this->_published[$path] = $this->getBaseUrl() . "/$dir/$fileName"; + } else { + if (is_dir($src)) { + $dir = $this->hash($hashByName ? basename($src) : $src . filemtime($src)); + $dstDir = $this->getBasePath() . DIRECTORY_SEPARATOR . $dir; - if($this->linkAssets) - { - if(!is_dir($dstDir)) - symlink($src,$dstDir); - } - else if(!is_dir($dstDir) || $forceCopy) - { - CFileHelper::copyDirectory($src,$dstDir,array( - 'exclude'=>$this->excludeFiles, - 'level'=>$level, - 'newDirMode'=>$this->newDirMode, - 'newFileMode'=>$this->newFileMode, - )); - } + if ($this->linkAssets) { + if (!is_dir($dstDir)) { + symlink($src, $dstDir); + } + } else { + if (!is_dir($dstDir) || $forceCopy) { + CFileHelper::copyDirectory($src, $dstDir, array( + 'exclude' => $this->excludeFiles, + 'level' => $level, + 'newDirMode' => $this->newDirMode, + 'newFileMode' => $this->newFileMode, + )); + } + } - return $this->_published[$path]=$this->getBaseUrl().'/'.$dir; + return $this->_published[$path] = $this->getBaseUrl() . '/' . $dir; + } + } } } throw new CException(Yii::t('yii|The asset "{asset}" to be published does not exist.', - array('{asset}'=>$path))); + array('{asset}' => $path))); } /** @@ -249,18 +226,18 @@ class CAssetManager extends CApplicationComponent * different extensions. * @return string the published file path. False if the file or directory does not exist */ - public function getPublishedPath($path,$hashByName=false) + public function getPublishedPath($path, $hashByName = false) { - if(($path=realpath($path))!==false) - { - $base=$this->getBasePath().DIRECTORY_SEPARATOR; - if(is_file($path)) - return $base . $this->hash($hashByName ? basename($path) : dirname($path).filemtime($path)) . DIRECTORY_SEPARATOR . basename($path); - else - return $base . $this->hash($hashByName ? basename($path) : $path.filemtime($path)); - } - else + if (($path = realpath($path)) !== false) { + $base = $this->getBasePath() . DIRECTORY_SEPARATOR; + if (is_file($path)) { + return $base . $this->hash($hashByName ? basename($path) : dirname($path) . filemtime($path)) . DIRECTORY_SEPARATOR . basename($path); + } else { + return $base . $this->hash($hashByName ? basename($path) : $path . filemtime($path)); + } + } else { return false; + } } /** @@ -274,19 +251,20 @@ class CAssetManager extends CApplicationComponent * different extensions. * @return string the published URL for the file or directory. False if the file or directory does not exist. */ - public function getPublishedUrl($path,$hashByName=false) + public function getPublishedUrl($path, $hashByName = false) { - if(isset($this->_published[$path])) + if (isset($this->_published[$path])) { return $this->_published[$path]; - if(($path=realpath($path))!==false) - { - if(is_file($path)) - return $this->getBaseUrl().'/'.$this->hash($hashByName ? basename($path) : dirname($path).filemtime($path)).'/'.basename($path); - else - return $this->getBaseUrl().'/'.$this->hash($hashByName ? basename($path) : $path.filemtime($path)); } - else + if (($path = realpath($path)) !== false) { + if (is_file($path)) { + return $this->getBaseUrl() . '/' . $this->hash($hashByName ? basename($path) : dirname($path) . filemtime($path)) . '/' . basename($path); + } else { + return $this->getBaseUrl() . '/' . $this->hash($hashByName ? basename($path) : $path . filemtime($path)); + } + } else { return false; + } } /** @@ -297,6 +275,6 @@ class CAssetManager extends CApplicationComponent */ protected function hash($path) { - return sprintf('%x',crc32($path.Yii::getVersion())); + return sprintf('%x', crc32($path . Yii::getVersion())); } } From 8f7757a25c963afaa0c366129e9812f04bed8853 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Tue, 16 Apr 2013 00:17:15 +0400 Subject: [PATCH 062/104] conditions cleanup --- framework/helpers/base/Html.php | 2 +- framework/widgets/ActiveForm.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/framework/helpers/base/Html.php b/framework/helpers/base/Html.php index 5e7f4ad..bb1fed3 100644 --- a/framework/helpers/base/Html.php +++ b/framework/helpers/base/Html.php @@ -728,7 +728,7 @@ class Html if (!isset($options['size'])) { $options['size'] = 4; } - if (isset($options['multiple']) && $options['multiple'] && substr($name, -2) !== '[]') { + if (!empty($options['multiple']) && substr($name, -2) !== '[]') { $name .= '[]'; } $options['name'] = $name; diff --git a/framework/widgets/ActiveForm.php b/framework/widgets/ActiveForm.php index 48bc181..8192bc0 100644 --- a/framework/widgets/ActiveForm.php +++ b/framework/widgets/ActiveForm.php @@ -64,7 +64,7 @@ class ActiveForm extends Widget $models = array($models); } - $showAll = isset($options['showAll']) && $options['showAll']; + $showAll = !empty($options['showAll']); $lines = array(); /** @var $model Model */ foreach ($models as $model) { From ac5b25e3f7514763ff60edfff9bb23346f0ec509 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Tue, 16 Apr 2013 00:52:24 +0400 Subject: [PATCH 063/104] fixed typos --- framework/caching/ZendDataCache.php | 2 +- framework/console/controllers/AppController.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/framework/caching/ZendDataCache.php b/framework/caching/ZendDataCache.php index 669733d..5b41a8d 100644 --- a/framework/caching/ZendDataCache.php +++ b/framework/caching/ZendDataCache.php @@ -10,7 +10,7 @@ namespace yii\caching; /** * ZendDataCache provides Zend data caching in terms of an application component. * - * To use this application component, the [Zend Data Cache PHP extensionn](http://www.zend.com/en/products/server/) + * To use this application component, the [Zend Data Cache PHP extension](http://www.zend.com/en/products/server/) * must be loaded. * * See [[Cache]] for common cache operations that ZendDataCache supports. diff --git a/framework/console/controllers/AppController.php b/framework/console/controllers/AppController.php index 93ef5f5..2c32c54 100644 --- a/framework/console/controllers/AppController.php +++ b/framework/console/controllers/AppController.php @@ -159,7 +159,7 @@ class AppController extends Controller * @param string $pathTo path to file we want to get relative path for * @param string $varName variable name w/o $ to replace value with relative path for * - * @return string target file contetns + * @return string target file contents */ public function replaceRelativePath($source, $pathTo, $varName) { From 9f2b44fc21c0dc6572cdda930dae96276cd80426 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Tue, 16 Apr 2013 00:53:07 +0400 Subject: [PATCH 064/104] phpdoc --- framework/base/ErrorHandler.php | 14 ++++++++++++++ framework/base/Response.php | 18 ++++++++++++++++++ framework/caching/DbDependency.php | 1 + framework/caching/MemCache.php | 2 +- framework/helpers/base/VarDumper.php | 2 +- framework/validators/CaptchaValidator.php | 1 + framework/widgets/ActiveForm.php | 16 +++++++++++++++- 7 files changed, 51 insertions(+), 3 deletions(-) diff --git a/framework/base/ErrorHandler.php b/framework/base/ErrorHandler.php index a2f372c..dc83474 100644 --- a/framework/base/ErrorHandler.php +++ b/framework/base/ErrorHandler.php @@ -51,6 +51,7 @@ class ErrorHandler extends Component /** + * Handles exception * @param \Exception $exception */ public function handle($exception) @@ -64,6 +65,10 @@ class ErrorHandler extends Component $this->renderException($exception); } + /** + * Renders exception + * @param \Exception $exception + */ protected function renderException($exception) { if ($this->errorAction !== null) { @@ -196,6 +201,10 @@ class ErrorHandler extends Component echo '
    ' . $output . '
    '; } + /** + * Renders calls stack trace + * @param array $trace + */ public function renderTrace($trace) { $count = 0; @@ -233,6 +242,11 @@ class ErrorHandler extends Component echo ''; } + /** + * Converts special characters to HTML entities + * @param string $text text to encode + * @return string + */ public function htmlEncode($text) { return htmlspecialchars($text, ENT_QUOTES, \Yii::$app->charset); diff --git a/framework/base/Response.php b/framework/base/Response.php index a53fd61..af91a20 100644 --- a/framework/base/Response.php +++ b/framework/base/Response.php @@ -13,27 +13,45 @@ namespace yii\base; */ class Response extends Component { + /** + * Starts output buffering + */ public function beginOutput() { ob_start(); ob_implicit_flush(false); } + /** + * Returns contents of the output buffer and discards it + * @return string output buffer contents + */ public function endOutput() { return ob_get_clean(); } + /** + * Returns contents of the output buffer + * @return string output buffer contents + */ public function getOutput() { return ob_get_contents(); } + /** + * Discards the output buffer + */ public function cleanOutput() { ob_clean(); } + /** + * Discards the output buffer + * @param boolean $all if true recursively discards all output buffers used + */ public function removeOutput($all = true) { if ($all) { diff --git a/framework/caching/DbDependency.php b/framework/caching/DbDependency.php index cbe0ae1..7d45223 100644 --- a/framework/caching/DbDependency.php +++ b/framework/caching/DbDependency.php @@ -52,6 +52,7 @@ class DbDependency extends Dependency /** * Generates the data needed to determine if dependency has been changed. * This method returns the value of the global state. + * @throws InvalidConfigException * @return mixed the data needed to determine if dependency has been changed. */ protected function generateDependencyData() diff --git a/framework/caching/MemCache.php b/framework/caching/MemCache.php index df07b8e..20aff21 100644 --- a/framework/caching/MemCache.php +++ b/framework/caching/MemCache.php @@ -106,7 +106,7 @@ class MemCache extends Cache /** * Returns the underlying memcache (or memcached) object. * @return \Memcache|\Memcached the memcache (or memcached) object used by this cache component. - * @throws Exception if memcache or memcached extension is not loaded + * @throws InvalidConfigException if memcache or memcached extension is not loaded */ public function getMemcache() { diff --git a/framework/helpers/base/VarDumper.php b/framework/helpers/base/VarDumper.php index 5942cd8..fe15d98 100644 --- a/framework/helpers/base/VarDumper.php +++ b/framework/helpers/base/VarDumper.php @@ -64,7 +64,7 @@ class VarDumper return self::$_output; } - /* + /** * @param mixed $var variable to be dumped * @param integer $level depth level */ diff --git a/framework/validators/CaptchaValidator.php b/framework/validators/CaptchaValidator.php index 3b4745b..ebb0039 100644 --- a/framework/validators/CaptchaValidator.php +++ b/framework/validators/CaptchaValidator.php @@ -68,6 +68,7 @@ class CaptchaValidator extends Validator /** * Returns the CAPTCHA action object. + * @throws InvalidConfigException * @return CaptchaAction the action object */ public function getCaptchaAction() diff --git a/framework/widgets/ActiveForm.php b/framework/widgets/ActiveForm.php index 8192bc0..83506dd 100644 --- a/framework/widgets/ActiveForm.php +++ b/framework/widgets/ActiveForm.php @@ -39,10 +39,16 @@ class ActiveForm extends Widget public $errorMessageClass = 'yii-error-message'; /** * @var string the default CSS class that indicates an input has error. - * This is */ public $errorClass = 'yii-error'; + /** + * @var string the default CSS class that indicates an input validated successfully. + */ public $successClass = 'yii-success'; + + /** + * @var string the default CSS class that indicates an input is currently being validated. + */ public $validatingClass = 'yii-validating'; /** * @var boolean whether to enable client-side data validation. Defaults to false. @@ -127,6 +133,14 @@ class ActiveForm extends Widget return Html::label($label, $for, $options); } + /** + * @param string $type + * @param Model $model + * @param string $attribute + * @param array $options + * + * @return string + */ public function input($type, $model, $attribute, $options = array()) { $value = $this->getAttributeValue($model, $attribute); From f2284948949d7c01b3971a7c464f2150aa2f3185 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Mon, 15 Apr 2013 16:57:54 -0400 Subject: [PATCH 065/104] script IWP --- framework/assets.php | 5 +++++ framework/web/AssetManager.php | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 framework/assets.php diff --git a/framework/assets.php b/framework/assets.php new file mode 100644 index 0000000..5efa94a --- /dev/null +++ b/framework/assets.php @@ -0,0 +1,5 @@ + __DIR__ . '/web/assets', +); \ No newline at end of file diff --git a/framework/web/AssetManager.php b/framework/web/AssetManager.php index 3cb1e83..ccf6dd8 100644 --- a/framework/web/AssetManager.php +++ b/framework/web/AssetManager.php @@ -96,10 +96,11 @@ class AssetManager extends Component /** * @param string $name + * @param boolean $publish * @return AssetBundle * @throws InvalidParamException */ - public function getBundle($name) + public function getBundle($name, $publish = true) { if (!isset($this->bundles[$name])) { $manifest = Yii::getAlias("@{$name}/assets.php", false); From 1e21bae999707cb356bc84cac41c7736bf6df727 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Tue, 16 Apr 2013 01:01:23 +0400 Subject: [PATCH 066/104] Removed Response::removeOutput, moved recursive buffer cleanup into Response::cleanOutput --- framework/base/Response.php | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/framework/base/Response.php b/framework/base/Response.php index af91a20..396b073 100644 --- a/framework/base/Response.php +++ b/framework/base/Response.php @@ -42,17 +42,9 @@ class Response extends Component /** * Discards the output buffer - */ - public function cleanOutput() - { - ob_clean(); - } - - /** - * Discards the output buffer * @param boolean $all if true recursively discards all output buffers used */ - public function removeOutput($all = true) + public function cleanOutput($all = true) { if ($all) { for ($level = ob_get_level(); $level > 0; --$level) { From 9edc942caf1817d7f47bdc121479ab54e7c48c72 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Mon, 15 Apr 2013 17:10:16 -0400 Subject: [PATCH 067/104] script WIP --- framework/web/AssetManager.php | 1 + 1 file changed, 1 insertion(+) diff --git a/framework/web/AssetManager.php b/framework/web/AssetManager.php index ccf6dd8..6c32687 100644 --- a/framework/web/AssetManager.php +++ b/framework/web/AssetManager.php @@ -116,6 +116,7 @@ class AssetManager extends Component $this->bundles[$name] = Yii::createObject($config); } } + // todo: publish bundle return $this->bundles[$name]; } From 0416e01414ccf1261f5fc8f88ff059f0857813bb Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Mon, 15 Apr 2013 22:59:13 -0400 Subject: [PATCH 068/104] script WIP --- framework/YiiBase.php | 30 ++++++++++++++++++++++++++++-- framework/base/Application.php | 16 ++++++++++++++-- framework/base/ViewContent.php | 3 ++- framework/web/Application.php | 12 ++++++++++++ framework/web/AssetManager.php | 27 ++++++++++++++++++++++----- 5 files changed, 78 insertions(+), 10 deletions(-) diff --git a/framework/YiiBase.php b/framework/YiiBase.php index fb2967a..c32cc28 100644 --- a/framework/YiiBase.php +++ b/framework/YiiBase.php @@ -198,6 +198,32 @@ class YiiBase } /** + * Returns the root alias part of a given alias. + * A root alias is an alias that has been registered via [[setAlias()]] previously. + * If a given alias matches multiple root aliases, the longest one will be returned. + * @param string $alias the alias + * @return string|boolean the root alias, or false if no root alias is found + */ + public static function getRootAlias($alias) + { + $pos = strpos($alias, '/'); + $root = $pos === false ? $alias : substr($alias, 0, $pos); + + if (isset(self::$aliases[$root])) { + if (is_string(self::$aliases[$root])) { + return $root; + } else { + foreach (self::$aliases[$root] as $name => $path) { + if (strpos($alias . '/', $name . '/') === 0) { + return $name; + } + } + } + } + return false; + } + + /** * Registers a path alias. * * A path alias is a short name representing a long path (a file path, a URL, etc.) @@ -222,13 +248,13 @@ class YiiBase * - a path alias (e.g. `@yii/base`). In this case, the path alias will be converted into the * actual path first by calling [[getAlias()]]. * - * @throws InvalidParamException the alias does not start with '@', or if $path is an invalid alias. + * @throws InvalidParamException if $path is an invalid alias. * @see getAlias */ public static function setAlias($alias, $path) { if (strncmp($alias, '@', 1)) { - throw new InvalidParamException('The alias must start with the "@" character.'); + $alias = '@' . $alias; } $pos = strpos($alias, '/'); $root = $pos === false ? $alias : substr($alias, 0, $pos); diff --git a/framework/base/Application.php b/framework/base/Application.php index b9f01d7..fd6527f 100644 --- a/framework/base/Application.php +++ b/framework/base/Application.php @@ -56,6 +56,11 @@ class Application extends Module * If this is false, layout will be disabled. */ public $layout = 'main'; + /** + * @var array list of installed extensions. The array keys are the extension names, and the array + * values are the corresponding extension root source directories or path aliases. + */ + public $extensions = array(); private $_ended = false; @@ -81,12 +86,19 @@ class Application extends Module if (isset($config['basePath'])) { $this->setBasePath($config['basePath']); + Yii::setAlias('@app', $this->getBasePath()); unset($config['basePath']); - Yii::$aliases['@app'] = $this->getBasePath(); } else { throw new InvalidConfigException('The "basePath" configuration is required.'); } + if (isset($config['extensions'])) { + foreach ($config['extensions'] as $name => $path) { + Yii::setAlias("@$name", $path); + } + unset($config['extensions']); + } + $this->registerErrorHandlers(); $this->registerCoreComponents(); @@ -206,7 +218,7 @@ class Application extends Module */ public function getVendorPath() { - if ($this->_vendorPath !== null) { + if ($this->_vendorPath === null) { $this->setVendorPath($this->getBasePath() . DIRECTORY_SEPARATOR . 'vendor'); } return $this->_vendorPath; diff --git a/framework/base/ViewContent.php b/framework/base/ViewContent.php index 797dba2..8b4e835 100644 --- a/framework/base/ViewContent.php +++ b/framework/base/ViewContent.php @@ -28,6 +28,7 @@ class ViewContent extends Component * @var \yii\web\AssetManager */ public $assetManager; + public $assetBundles; public $title; public $metaTags; @@ -45,7 +46,7 @@ class ViewContent extends Component { parent::init(); if ($this->assetManager === null) { - $this->assetManager = Yii::$app->getAssetManager(); + $this->assetManager = Yii::$app->getAssets(); } } diff --git a/framework/web/Application.php b/framework/web/Application.php index f9b615d..32f6479 100644 --- a/framework/web/Application.php +++ b/framework/web/Application.php @@ -98,6 +98,15 @@ class Application extends \yii\base\Application } /** + * Returns the asset manager. + * @return AssetManager the asset manager component + */ + public function getAssets() + { + return $this->getComponent('user'); + } + + /** * Registers the core application components. * @see setComponents */ @@ -117,6 +126,9 @@ class Application extends \yii\base\Application 'user' => array( 'class' => 'yii\web\User', ), + 'assets' => array( + 'class' => 'yii\web\AssetManager', + ), )); } } diff --git a/framework/web/AssetManager.php b/framework/web/AssetManager.php index 6c32687..1f82e7c 100644 --- a/framework/web/AssetManager.php +++ b/framework/web/AssetManager.php @@ -91,7 +91,13 @@ class AssetManager extends Component } else { $this->base = realpath($this->basePath); } - $this->baseUrl = rtrim(Yii::getAlias($this->getBaseUrl), '/'); + $this->baseUrl = rtrim(Yii::getAlias($this->baseUrl), '/'); + + foreach (require(YII_PATH . '/assets.php') as $name => $bundle) { + if (!isset($this->bundles[$name])) { + $this->bundles[$name] = $bundle; + } + } } /** @@ -103,11 +109,18 @@ class AssetManager extends Component public function getBundle($name, $publish = true) { if (!isset($this->bundles[$name])) { - $manifest = Yii::getAlias("@{$name}/assets.php", false); - if ($manifest === false) { + $rootAlias = Yii::getRootAlias("@$name"); + if ($rootAlias !== false) { + $manifest = Yii::getAlias("$rootAlias/assets.php", false); + if ($manifest !== false && is_file($manifest)) { + foreach (require($manifest) as $bn => $config) { + $this->bundles[$bn] = $config; + } + } + } + if (!isset($this->bundles[$name])) { throw new InvalidParamException("Unable to find the asset bundle: $name"); } - $this->bundles[$name] = require($manifest); } if (is_array($this->bundles[$name])) { $config = $this->bundles[$name]; @@ -116,7 +129,11 @@ class AssetManager extends Component $this->bundles[$name] = Yii::createObject($config); } } - // todo: publish bundle + + if ($publish) { + + } + return $this->bundles[$name]; } From ee2d93b1813244e46c238ca955a52d5eb59f8d30 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Tue, 16 Apr 2013 07:59:31 -0400 Subject: [PATCH 069/104] scripts WIP --- framework/base/ViewContent.php | 2 +- framework/web/AssetBundle.php | 20 +++++++++++++++++++- framework/web/AssetManager.php | 16 +++++++++++++++- 3 files changed, 35 insertions(+), 3 deletions(-) diff --git a/framework/base/ViewContent.php b/framework/base/ViewContent.php index 8b4e835..a2951e7 100644 --- a/framework/base/ViewContent.php +++ b/framework/base/ViewContent.php @@ -98,7 +98,7 @@ class ViewContent extends Component $bundle = $this->assetManager->getBundle($name); if ($bundle !== null) { $this->assetBundles[$name] = false; - $bundle->registerWith($this); + $bundle->registerAssets($this); $this->assetBundles[$name] = true; } else { throw new InvalidConfigException("Unknown asset bundle: $name"); diff --git a/framework/web/AssetBundle.php b/framework/web/AssetBundle.php index 3e4541e..26fbb34 100644 --- a/framework/web/AssetBundle.php +++ b/framework/web/AssetBundle.php @@ -42,10 +42,18 @@ class AssetBundle extends Object public $css = array(); public $depends = array(); + public function mapTo($target) + { + $this->depends = array($target); + $this->js = $this->css = array(); + $this->basePath = null; + $this->baseUrl = null; + } + /** * @param \yii\base\ViewContent $content */ - public function registerWith($content) + public function registerAssets($content) { foreach ($this->depends as $name) { $content->registerAssetBundle($name); @@ -65,4 +73,14 @@ class AssetBundle extends Object } } } + + /** + * @param \yii\web\AssetManager $assetManager + */ + public function publish($assetManager) + { + if ($this->basePath !== null && $this->baseUrl === null) { + return; + } + } } \ No newline at end of file diff --git a/framework/web/AssetManager.php b/framework/web/AssetManager.php index 1f82e7c..4f44f0c 100644 --- a/framework/web/AssetManager.php +++ b/framework/web/AssetManager.php @@ -129,9 +129,23 @@ class AssetManager extends Component $this->bundles[$name] = Yii::createObject($config); } } + /** @var $bundle AssetBundle */ + $bundle = $this->bundles[$name]; + if (isset($this->bundleMap[$name]) && is_string($this->bundleMap[$name])) { + $target = $this->bundleMap[$name]; + if (!isset($this->bundles[$target])) { + if (isset($this->bundleMap[$target])) { + $this->bundles[$target] = $this->bundleMap[$target]; + } else { + throw new InvalidConfigException("Asset bundle '$name' is mapped to an unknown bundle: $target"); + } + } + $bundle->mapTo($target); + unset($this->bundleMap[$name]); + } if ($publish) { - + $bundle->publish($this); } return $this->bundles[$name]; From 5b412b8f84f7351889b9b046bc484d98b9ec017b Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Tue, 16 Apr 2013 17:42:31 -0400 Subject: [PATCH 070/104] script WIP --- framework/base/Application.php | 11 +- framework/base/Controller.php | 4 +- framework/base/Module.php | 14 +- framework/base/Theme.php | 6 +- framework/base/Widget.php | 2 +- framework/console/controllers/AppController.php | 121 +++++++++++++++- framework/helpers/base/FileHelper.php | 177 ++---------------------- framework/validators/FileValidator.php | 2 +- framework/web/AssetBundle.php | 65 ++++++--- framework/web/AssetManager.php | 115 +++++++-------- 10 files changed, 251 insertions(+), 266 deletions(-) diff --git a/framework/base/Application.php b/framework/base/Application.php index fd6527f..e0d0237 100644 --- a/framework/base/Application.php +++ b/framework/base/Application.php @@ -202,11 +202,11 @@ class Application extends Module */ public function setRuntimePath($path) { - $p = FileHelper::ensureDirectory($path); - if (is_writable($p)) { - $this->_runtimePath = $p; + $path = Yii::getAlias($path); + if (is_dir($path) && is_writable($path)) { + $this->_runtimePath = $path; } else { - throw new InvalidConfigException("Runtime path must be writable by the Web server process: $path"); + throw new InvalidConfigException("Runtime path must be a directory writable by the Web server process: $path"); } } @@ -227,11 +227,10 @@ class Application extends Module /** * Sets the directory that stores vendor files. * @param string $path the directory that stores vendor files. - * @throws InvalidConfigException if the directory does not exist */ public function setVendorPath($path) { - $this->_vendorPath = FileHelper::ensureDirectory($path); + $this->_vendorPath = Yii::getAlias($path); } /** diff --git a/framework/base/Controller.php b/framework/base/Controller.php index 6ff68da..d11d19b 100644 --- a/framework/base/Controller.php +++ b/framework/base/Controller.php @@ -428,7 +428,7 @@ class Controller extends Component $file = $this->getViewPath() . DIRECTORY_SEPARATOR . $view; } - return FileHelper::getExtension($file) === '' ? $file . '.php' : $file; + return pathinfo($file, PATHINFO_EXTENSION) === '' ? $file . '.php' : $file; } /** @@ -463,7 +463,7 @@ class Controller extends Component $file = $module->getLayoutPath() . DIRECTORY_SEPARATOR . $view; } - if (FileHelper::getExtension($file) === '') { + if (pathinfo($file, PATHINFO_EXTENSION) === '') { $file .= '.php'; } return $file; diff --git a/framework/base/Module.php b/framework/base/Module.php index 0b2bd16..ee97614 100644 --- a/framework/base/Module.php +++ b/framework/base/Module.php @@ -211,7 +211,13 @@ abstract class Module extends Component */ public function setBasePath($path) { - $this->_basePath = FileHelper::ensureDirectory($path); + $path = Yii::getAlias($path); + $p = realpath($path); + if ($p !== false && is_dir($p)) { + $this->_basePath = $p; + } else { + throw new InvalidParamException("The directory does not exist: $path"); + } } /** @@ -236,7 +242,7 @@ abstract class Module extends Component */ public function setControllerPath($path) { - $this->_controllerPath = FileHelper::ensureDirectory($path); + $this->_controllerPath = Yii::getAlias($path); } /** @@ -259,7 +265,7 @@ abstract class Module extends Component */ public function setViewPath($path) { - $this->_viewPath = FileHelper::ensureDirectory($path); + $this->_viewPath = Yii::getAlias($path); } /** @@ -282,7 +288,7 @@ abstract class Module extends Component */ public function setLayoutPath($path) { - $this->_layoutPath = FileHelper::ensureDirectory($path); + $this->_layoutPath = Yii::getAlias($path); } /** diff --git a/framework/base/Theme.php b/framework/base/Theme.php index e529a63..a60d56e 100644 --- a/framework/base/Theme.php +++ b/framework/base/Theme.php @@ -61,10 +61,10 @@ class Theme extends Component parent::init(); if (empty($this->pathMap)) { if ($this->basePath !== null) { - $this->basePath = FileHelper::ensureDirectory($this->basePath); + $this->basePath = Yii::getAlias($this->basePath); $this->pathMap = array(Yii::$app->getBasePath() => $this->basePath); } else { - throw new InvalidConfigException("Theme::basePath must be set."); + throw new InvalidConfigException('The "basePath" property must be set.'); } } $paths = array(); @@ -75,7 +75,7 @@ class Theme extends Component } $this->pathMap = $paths; if ($this->baseUrl === null) { - throw new InvalidConfigException("Theme::baseUrl must be set."); + throw new InvalidConfigException('The "baseUrl" property must be set.'); } else { $this->baseUrl = rtrim(Yii::getAlias($this->baseUrl), '/'); } diff --git a/framework/base/Widget.php b/framework/base/Widget.php index c6667fa..13e6d30 100644 --- a/framework/base/Widget.php +++ b/framework/base/Widget.php @@ -131,6 +131,6 @@ class Widget extends Component $file = $this->getViewPath() . DIRECTORY_SEPARATOR . ltrim($view, '/'); } - return FileHelper::getExtension($file) === '' ? $file . '.php' : $file; + return pathinfo($file, PATHINFO_EXTENSION) === '' ? $file . '.php' : $file; } } \ No newline at end of file diff --git a/framework/console/controllers/AppController.php b/framework/console/controllers/AppController.php index 2c32c54..237ba0f 100644 --- a/framework/console/controllers/AppController.php +++ b/framework/console/controllers/AppController.php @@ -86,7 +86,7 @@ class AppController extends Controller $sourceDir = $this->getSourceDir(); $config = $this->getConfig(); - $list = FileHelper::buildFileList($sourceDir, $path); + $list = $this->buildFileList($sourceDir, $path); if(is_array($config)) { foreach($config as $file => $settings) { @@ -96,7 +96,7 @@ class AppController extends Controller } } - FileHelper::copyFiles($list); + $this->copyFiles($list); if(is_array($config)) { foreach($config as $file => $settings) { @@ -204,4 +204,121 @@ class AppController extends Controller return '__DIR__.\''.$up.'/'.basename($path1).'\''; } + + + /** + * Copies a list of files from one place to another. + * @param array $fileList the list of files to be copied (name=>spec). + * The array keys are names displayed during the copy process, and array values are specifications + * for files to be copied. Each array value must be an array of the following structure: + *
      + *
    • 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 + */ + protected function copyFiles($fileList) + { + $overwriteAll = false; + foreach($fileList as $name=>$file) { + $source = strtr($file['source'], '/\\', DIRECTORY_SEPARATOR); + $target = strtr($file['target'], '/\\', DIRECTORY_SEPARATOR); + $callback = isset($file['callback']) ? $file['callback'] : null; + $params = isset($file['params']) ? $file['params'] : null; + + if(is_dir($source)) { + if (!is_dir($target)) { + mkdir($target, 0777, true); + } + continue; + } + + if($callback !== null) { + $content = call_user_func($callback, $source, $params); + } + else { + $content = file_get_contents($source); + } + if(is_file($target)) { + if($content === file_get_contents($target)) { + echo " unchanged $name\n"; + continue; + } + if($overwriteAll) { + echo " overwrite $name\n"; + } + else { + echo " exist $name\n"; + echo " ...overwrite? [Yes|No|All|Quit] "; + $answer = trim(fgets(STDIN)); + if(!strncasecmp($answer, 'q', 1)) { + return; + } + elseif(!strncasecmp($answer, 'y', 1)) { + echo " overwrite $name\n"; + } + elseif(!strncasecmp($answer, 'a', 1)) { + echo " overwrite $name\n"; + $overwriteAll = true; + } + else { + echo " skip $name\n"; + continue; + } + } + } + else { + if (!is_dir(dirname($target))) { + mkdir(dirname($target), 0777, true); + } + echo " generate $name\n"; + } + file_put_contents($target, $content); + } + } + + /** + * Builds the file list of a directory. + * This method traverses through the specified directory and builds + * a list of files and subdirectories that the directory contains. + * The result of this function can be passed to {@link copyFiles}. + * @param string $sourceDir the source directory + * @param string $targetDir the target directory + * @param string $baseDir base directory + * @param array $ignoreFiles list of the names of files that should + * be ignored in list building process. 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}) + */ + protected function buildFileList($sourceDir, $targetDir, $baseDir='', $ignoreFiles=array(), $renameMap=array()) + { + $list = array(); + $handle = opendir($sourceDir); + while(($file = readdir($handle)) !== false) { + if(in_array($file, array('.', '..', '.svn', '.gitignore')) || 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/base/FileHelper.php b/framework/helpers/base/FileHelper.php index 7dd5543..5bab36f 100644 --- a/framework/helpers/base/FileHelper.php +++ b/framework/helpers/base/FileHelper.php @@ -9,8 +9,7 @@ namespace yii\helpers\base; -use yii\base\Exception; -use yii\base\InvalidConfigException; +use Yii; /** * Filesystem helper @@ -22,35 +21,6 @@ use yii\base\InvalidConfigException; class FileHelper { /** - * Returns the extension name of a file path. - * For example, the path "path/to/something.php" would return "php". - * @param string $path the file path - * @return string the extension name without the dot character. - */ - public static function getExtension($path) - { - return pathinfo($path, PATHINFO_EXTENSION); - } - - /** - * Checks the given path and ensures it is a directory. - * This method will call `realpath()` to "normalize" the given path. - * If the given path does not refer to an existing directory, an exception will be thrown. - * @param string $path the given path. This can also be a path alias. - * @return string the normalized path - * @throws InvalidConfigException if the path does not refer to an existing directory. - */ - public static function ensureDirectory($path) - { - $p = \Yii::getAlias($path); - if (($p = realpath($p)) !== false && is_dir($p)) { - return $p; - } else { - throw new InvalidConfigException('Directory does not exist: ' . $path); - } - } - - /** * Normalizes a file/directory path. * After normalization, the directory separators in the path will be `DIRECTORY_SEPARATOR`, * and any trailing directory separators will be removed. For example, '/home\demo/' on Linux @@ -69,17 +39,14 @@ class FileHelper * * 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 + * whose name is the 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. @@ -91,10 +58,10 @@ class FileHelper public static function localize($file, $language = null, $sourceLanguage = null) { if ($language === null) { - $language = \Yii::$app->language; + $language = Yii::$app->language; } if ($sourceLanguage === null) { - $sourceLanguage = \Yii::$app->sourceLanguage; + $sourceLanguage = Yii::$app->sourceLanguage; } if ($language === $sourceLanguage) { return $file; @@ -120,6 +87,7 @@ class FileHelper if (function_exists('finfo_open')) { $info = finfo_open(FILEINFO_MIME_TYPE, $magicFile); if ($info && ($result = finfo_file($info, $file)) !== false) { + finfo_close($info); return $result; } } @@ -137,138 +105,21 @@ class FileHelper */ public static function getMimeTypeByExtension($file, $magicFile = null) { + static $mimeTypes = array(); if ($magicFile === null) { - $magicFile = \Yii::getAlias('@yii/util/mimeTypes.php'); + $magicFile = __DIR__ . '/mimeTypes.php'; + } + if (!isset($mimeTypes[$magicFile])) { + $mimeTypes[$magicFile] = require($magicFile); } - $mimeTypes = require($magicFile); if (($ext = pathinfo($file, PATHINFO_EXTENSION)) !== '') { $ext = strtolower($ext); - if (isset($mimeTypes[$ext])) { - return $mimeTypes[$ext]; + if (isset($mimeTypes[$magicFile][$ext])) { + return $mimeTypes[$magicFile][$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/validators/FileValidator.php b/framework/validators/FileValidator.php index c104c05..b3de0b2 100644 --- a/framework/validators/FileValidator.php +++ b/framework/validators/FileValidator.php @@ -175,7 +175,7 @@ class FileValidator extends Validator if ($this->minSize !== null && $file->getSize() < $this->minSize) { $this->addError($object, $attribute, $this->tooSmall, array('{file}' => $file->getName(), '{limit}' => $this->minSize)); } - if (!empty($this->types) && !in_array(strtolower(FileHelper::getExtension($file->getName())), $this->types, true)) { + if (!empty($this->types) && !in_array(strtolower(pathinfo($file->getName(), PATHINFO_EXTENSION)), $this->types, true)) { $this->addError($object, $attribute, $this->wrongType, array('{file}' => $file->getName(), '{extensions}' => implode(', ', $this->types))); } break; diff --git a/framework/web/AssetBundle.php b/framework/web/AssetBundle.php index 26fbb34..d3e29d8 100644 --- a/framework/web/AssetBundle.php +++ b/framework/web/AssetBundle.php @@ -7,6 +7,7 @@ namespace yii\web; +use Yii; use yii\base\Object; /** @@ -36,18 +37,47 @@ use yii\base\Object; */ class AssetBundle extends Object { + /** + * @var string the root directory of the asset files. If this is not set, + * the assets are considered to be located under a Web-accessible folder already + * and no asset publishing will be performed. + */ public $basePath; - public $baseUrl; // if missing, the bundle will be published to the "www/assets" folder + /** + * @var string the base URL that will be prefixed to the asset files. + * This property must be set if [[basePath]] is not set. + * When this property is not set, it will be initialized as the base URL + * that the assets are published to. + */ + public $baseUrl; + /** + * @var array list of JavaScript files that this bundle contains. Each JavaScript file can + * be specified in one of the three formats: + * + * - a relative path: a path relative to [[basePath]] if [[basePath]] is set, + * or a URL relative to [[baseUrl]] if [[basePath]] is not set; + * - an absolute URL; + * - a path alias that can be resolved into a relative path or an absolute URL. + * + * Note that you should not use backward slashes "\" to specify JavaScript files. + * + * A JavaScript file can be associated with the options: // todo + */ public $js = array(); public $css = array(); + /** + * @var array list of the bundle names that this bundle depends on + */ public $depends = array(); - public function mapTo($target) + public function init() { - $this->depends = array($target); - $this->js = $this->css = array(); - $this->basePath = null; - $this->baseUrl = null; + if ($this->baseUrl !== null) { + $this->baseUrl = rtrim(Yii::getAlias($this->baseUrl), '/'); + } + if ($this->basePath !== null) { + $this->basePath = rtrim(Yii::getAlias($this->basePath), '/\\'); + } } /** @@ -59,18 +89,18 @@ class AssetBundle extends Object $content->registerAssetBundle($name); } foreach ($this->js as $js => $options) { - if (is_array($options)) { - $content->registerJsFile($js, $options); - } else { - $content->registerJsFile($options); + $js = is_string($options) ? $options : $js; + if (strpos($js, '//') !== 0 && strpos($js, '://') === false) { + $js = $this->baseUrl . '/' . ltrim($js, '/'); } + $content->registerJsFile($js, is_array($options) ? $options : array()); } foreach ($this->css as $css => $options) { - if (is_array($options)) { - $content->registerCssFile($css, $options); - } else { - $content->registerCssFile($options); + $css = is_string($options) ? $options : $css; + if (strpos($css, '//') !== 0 && strpos($css, '://') === false) { + $css = $this->baseUrl . '/' . ltrim($css, '/'); } + $content->registerCssFile($css, is_array($options) ? $options : array()); } } @@ -79,8 +109,11 @@ class AssetBundle extends Object */ public function publish($assetManager) { - if ($this->basePath !== null && $this->baseUrl === null) { - return; + if ($this->basePath !== null) { + $baseUrl = $assetManager->publish($this->basePath); + if ($this->baseUrl === null) { + $this->baseUrl = $baseUrl; + } } } } \ No newline at end of file diff --git a/framework/web/AssetManager.php b/framework/web/AssetManager.php index 4f44f0c..1390f47 100644 --- a/framework/web/AssetManager.php +++ b/framework/web/AssetManager.php @@ -26,10 +26,6 @@ class AssetManager extends Component */ public $bundles; /** - * @var - */ - public $bundleMap; - /** * @return string the root directory storing the published asset files. */ public $basePath = '@wwwroot/assets'; @@ -89,7 +85,7 @@ class AssetManager extends Component } elseif (!is_writable($this->basePath)) { throw new InvalidConfigException("The directory is not writable by the Web process: {$this->basePath}"); } else { - $this->base = realpath($this->basePath); + $this->basePath = realpath($this->basePath); } $this->baseUrl = rtrim(Yii::getAlias($this->baseUrl), '/'); @@ -131,18 +127,6 @@ class AssetManager extends Component } /** @var $bundle AssetBundle */ $bundle = $this->bundles[$name]; - if (isset($this->bundleMap[$name]) && is_string($this->bundleMap[$name])) { - $target = $this->bundleMap[$name]; - if (!isset($this->bundles[$target])) { - if (isset($this->bundleMap[$target])) { - $this->bundles[$target] = $this->bundleMap[$target]; - } else { - throw new InvalidConfigException("Asset bundle '$name' is mapped to an unknown bundle: $target"); - } - } - $bundle->mapTo($target); - unset($this->bundleMap[$name]); - } if ($publish) { $bundle->publish($this); @@ -191,61 +175,56 @@ class AssetManager extends Component { if (isset($this->_published[$path])) { return $this->_published[$path]; - } else { - if (($src = realpath($path)) !== false) { - if (is_file($src)) { - $dir = $this->hash($hashByName ? basename($src) : dirname($src) . filemtime($src)); - $fileName = basename($src); - $dstDir = $this->getBasePath() . DIRECTORY_SEPARATOR . $dir; - $dstFile = $dstDir . DIRECTORY_SEPARATOR . $fileName; + } - if ($this->linkAssets) { - if (!is_file($dstFile)) { - if (!is_dir($dstDir)) { - mkdir($dstDir); - @chmod($dstDir, $this->newDirMode); - } - symlink($src, $dstFile); - } - } else { - if (@filemtime($dstFile) < @filemtime($src)) { - if (!is_dir($dstDir)) { - mkdir($dstDir); - @chmod($dstDir, $this->newDirMode); - } - copy($src, $dstFile); - @chmod($dstFile, $this->newFileMode); - } - } + $src = realpath($path); + if ($src === false) { + throw new InvalidParamException("The file or directory to be published does not exist: $path"); + } + + if (is_file($src)) { + $dir = $this->hash($hashByName ? basename($src) : dirname($src) . filemtime($src)); + $fileName = basename($src); + $dstDir = $this->basePath . DIRECTORY_SEPARATOR . $dir; + $dstFile = $dstDir . DIRECTORY_SEPARATOR . $fileName; - return $this->_published[$path] = $this->getBaseUrl() . "/$dir/$fileName"; - } else { - if (is_dir($src)) { - $dir = $this->hash($hashByName ? basename($src) : $src . filemtime($src)); - $dstDir = $this->getBasePath() . DIRECTORY_SEPARATOR . $dir; + if (!is_dir($dstDir)) { + @mkdir($dstDir, $this->newDirMode, true); + } - if ($this->linkAssets) { - if (!is_dir($dstDir)) { - symlink($src, $dstDir); - } - } else { - if (!is_dir($dstDir) || $forceCopy) { - CFileHelper::copyDirectory($src, $dstDir, array( - 'exclude' => $this->excludeFiles, - 'level' => $level, - 'newDirMode' => $this->newDirMode, - 'newFileMode' => $this->newFileMode, - )); - } - } - return $this->_published[$path] = $this->getBaseUrl() . '/' . $dir; - } + if ($this->linkAssets) { + if (!is_file($dstFile)) { + symlink($src, $dstFile); + } + } elseif (@filemtime($dstFile) < @filemtime($src)) { + copy($src, $dstFile); + @chmod($dstFile, $this->newFileMode); + } + + $url = $this->baseUrl . "/$dir/$fileName"; + } else { + $dir = $this->hash($hashByName ? basename($src) : $src . filemtime($src)); + $dstDir = $this->basePath . DIRECTORY_SEPARATOR . $dir; + + if ($this->linkAssets) { + if (!is_dir($dstDir)) { + symlink($src, $dstDir); + } + } else { + if (!is_dir($dstDir) || $forceCopy) { + FileHelper::copyDirectory($src, $dstDir, array( + 'exclude' => $this->excludeFiles, + 'level' => $level, + 'newDirMode' => $this->newDirMode, + 'newFileMode' => $this->newFileMode, + )); } } + + $url = $this->baseUrl . '/' . $dir; } - throw new CException(Yii::t('yii|The asset "{asset}" to be published does not exist.', - array('{asset}' => $path))); + return $this->_published[$path] = $url; } /** @@ -262,7 +241,7 @@ class AssetManager extends Component public function getPublishedPath($path, $hashByName = false) { if (($path = realpath($path)) !== false) { - $base = $this->getBasePath() . DIRECTORY_SEPARATOR; + $base = $this->basePath . DIRECTORY_SEPARATOR; if (is_file($path)) { return $base . $this->hash($hashByName ? basename($path) : dirname($path) . filemtime($path)) . DIRECTORY_SEPARATOR . basename($path); } else { @@ -291,9 +270,9 @@ class AssetManager extends Component } if (($path = realpath($path)) !== false) { if (is_file($path)) { - return $this->getBaseUrl() . '/' . $this->hash($hashByName ? basename($path) : dirname($path) . filemtime($path)) . '/' . basename($path); + return $this->baseUrl . '/' . $this->hash($hashByName ? basename($path) : dirname($path) . filemtime($path)) . '/' . basename($path); } else { - return $this->getBaseUrl() . '/' . $this->hash($hashByName ? basename($path) : $path . filemtime($path)); + return $this->baseUrl . '/' . $this->hash($hashByName ? basename($path) : $path . filemtime($path)); } } else { return false; From 8f9cfa2c304f347ac66ba32c7dfdea00feae0bb3 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Tue, 16 Apr 2013 21:26:19 -0400 Subject: [PATCH 071/104] draft of script/asset management is done. --- framework/helpers/base/FileHelper.php | 52 +++++++++++++++- framework/web/AssetBundle.php | 40 ++++++------ framework/web/AssetManager.php | 111 +++++++++++++++------------------- 3 files changed, 116 insertions(+), 87 deletions(-) diff --git a/framework/helpers/base/FileHelper.php b/framework/helpers/base/FileHelper.php index 5bab36f..d3804fb 100644 --- a/framework/helpers/base/FileHelper.php +++ b/framework/helpers/base/FileHelper.php @@ -86,9 +86,12 @@ class FileHelper { if (function_exists('finfo_open')) { $info = finfo_open(FILEINFO_MIME_TYPE, $magicFile); - if ($info && ($result = finfo_file($info, $file)) !== false) { + if ($info) { + $result = finfo_file($info, $file); finfo_close($info); - return $result; + if ($result !== false) { + return $result; + } } } @@ -122,4 +125,49 @@ class FileHelper } + /** + * Copies a whole directory as another one. + * The files and sub-directories will also be copied over. + * @param string $src the source directory + * @param string $dst the destination directory + * @param array $options options for directory copy. Valid options are: + * + * - dirMode: integer, the permission to be set for newly copied directories. Defaults to 0777. + * - fileMode: integer, the permission to be set for newly copied files. Defaults to the current environment setting. + * - filter: callback, a PHP callback that is called for every sub-directory and file to + * determine if it should be copied. The signature of the callback should be: + * + * ~~~ + * // $path is the file/directory path to be copied + * function ($path) { + * // return a boolean indicating if $path should be copied + * } + * ~~~ + */ + public static function copyDirectory($src, $dst, $options = array()) + { + if (!is_dir($dst)) { + mkdir($dst, isset($options['dirMode']) ? $options['dirMode'] : 0777, true); + } + + $handle = opendir($src); + while (($file = readdir($handle)) !== false) { + if ($file === '.' || $file === '..') { + continue; + } + $srcPath = $src . DIRECTORY_SEPARATOR . $file; + if (!isset($options['filter']) || call_user_func($options['filter'], $srcPath)) { + $dstPath = $dst . DIRECTORY_SEPARATOR . $file; + if (is_file($srcPath)) { + copy($srcPath, $dstPath); + if (isset($options['fileMode'])) { + chmod($dstPath, $options['fileMode']); + } + } else { + static::copyDirectory($srcPath, $dstPath, $options); + } + } + } + closedir($handle); + } } \ No newline at end of file diff --git a/framework/web/AssetBundle.php b/framework/web/AssetBundle.php index d3e29d8..a6b69d2 100644 --- a/framework/web/AssetBundle.php +++ b/framework/web/AssetBundle.php @@ -11,27 +11,6 @@ use Yii; use yii\base\Object; /** - * Each asset bundle should be declared with the following structure: - * - * ~~~ - * array( - * 'basePath' => '...', - * 'baseUrl' => '...', // if missing, the bundle will be published to the "www/assets" folder - * 'js' => array( - * 'js/main.js', - * 'js/menu.js', - * 'js/base.js' => self::POS_HEAD, - * 'css' => array( - * 'css/main.css', - * 'css/menu.css', - * ), - * 'depends' => array( - * 'jquery', - * 'yii', - * 'yii/treeview', - * ), - * ) - * ~~~ * @author Qiang Xue * @since 2.0 */ @@ -61,9 +40,26 @@ class AssetBundle extends Object * * Note that you should not use backward slashes "\" to specify JavaScript files. * - * A JavaScript file can be associated with the options: // todo + * Each JavaScript file may be associated with options. In this case, the array key + * should be the JavaScript file path, while the corresponding array value should + * be the option array. The options will be passed to [[ViewContent::registerJsFile()]]. */ public $js = array(); + /** + * @var array list of CSS files that this bundle contains. Each CSS file can + * be specified in one of the three formats: + * + * - a relative path: a path relative to [[basePath]] if [[basePath]] is set, + * or a URL relative to [[baseUrl]] if [[basePath]] is not set; + * - an absolute URL; + * - a path alias that can be resolved into a relative path or an absolute URL. + * + * Note that you should not use backward slashes "\" to specify CSS files. + * + * Each CSS file may be associated with options. In this case, the array key + * should be the CSS file path, while the corresponding array value should + * be the option array. The options will be passed to [[ViewContent::registerCssFile()]]. + */ public $css = array(); /** * @var array list of the bundle names that this bundle depends on diff --git a/framework/web/AssetManager.php b/framework/web/AssetManager.php index 1390f47..c06d6b2 100644 --- a/framework/web/AssetManager.php +++ b/framework/web/AssetManager.php @@ -11,6 +11,7 @@ use Yii; use yii\base\Component; use yii\base\InvalidConfigException; use yii\base\InvalidParamException; +use yii\helpers\FileHelper; /** * @@ -58,24 +59,22 @@ class AssetManager extends Component **/ public $excludeFiles = array('.svn', '.gitignore'); /** - * @var integer the permission to be set for newly generated asset files. - * This value will be used by PHP chmod function. - * Defaults to 0666, meaning the file is read-writable by all users. - * @since 1.1.8 + * @var integer the permission to be set for newly published asset files. + * This value will be used by PHP chmod() function. + * If not set, the permission will be determined by the current environment. */ - public $newFileMode = 0666; + public $fileMode; /** * @var integer the permission to be set for newly generated asset directories. - * This value will be used by PHP chmod function. + * This value will be used by PHP chmod() function. * Defaults to 0777, meaning the directory can be read, written and executed by all users. - * @since 1.1.8 */ - public $newDirMode = 0777; + public $dirMode = 0777; + /** - * @var array published assets + * Initializes the component. + * @throws InvalidConfigException if [[basePath]] is invalid */ - private $_published = array(); - public function init() { parent::init(); @@ -136,16 +135,22 @@ class AssetManager extends Component } /** + * @var array published assets + */ + private $_published = array(); + + /** * Publishes a file or a directory. - * This method will copy the specified asset to a web accessible directory - * and return the URL for accessing the published asset. - *
      - *
    • If the asset is a file, its file modification time will be checked - * to avoid unnecessary file copying;
    • - *
    • If the asset is a directory, all files and subdirectories under it will - * be published recursively. Note, in case $forceCopy is false the method only checks the - * existence of the target directory to avoid repetitive copying.
    • - *
    + * + * This method will copy the specified file or directory to [[basePath]] so that + * it can be accessed via the Web server. + * + * If the asset is a file, its file modification time will be checked to avoid + * unnecessary file copying. + * + * If the asset is a directory, all files and subdirectories under it will be published recursively. + * Note, in case $forceCopy is false the method only checks the existence of the target + * directory to avoid repetitive copying (which is very expensive). * * Note: On rare scenario, a race condition can develop that will lead to a * one-time-manifestation of a non-critical problem in the creation of the directory @@ -155,23 +160,14 @@ class AssetManager extends Component * discussion: http://code.google.com/p/yii/issues/detail?id=2579 * * @param string $path the asset (file or directory) to be published - * @param boolean $hashByName whether the published directory should be named as the hashed basename. - * If false, the name will be the hash taken from dirname of the path being published and path mtime. - * Defaults to false. Set true if the path being published is shared among - * different extensions. - * @param integer $level level of recursive copying when the asset is a directory. - * Level -1 means publishing all subdirectories and files; - * Level 0 means publishing only the files DIRECTLY under the directory; - * level N means copying those directories that are within N levels. - * @param boolean $forceCopy whether we should copy the asset file or directory even if it is already published before. - * This parameter is set true mainly during development stage when the original - * assets are being constantly changed. The consequence is that the performance + * @param boolean $forceCopy whether the asset should ALWAYS be copied even if it is found + * in the target directory. This parameter is mainly useful during the development stage + * when the original assets are being constantly changed. The consequence is that the performance * is degraded, which is not a concern during development, however. - * This parameter has been available since version 1.1.2. * @return string an absolute URL to the published asset - * @throws CException if the asset to be published does not exist. + * @throws InvalidParamException if the asset to be published does not exist. */ - public function publish($path, $hashByName = false, $level = -1, $forceCopy = false) + public function publish($path, $forceCopy = false) { if (isset($this->_published[$path])) { return $this->_published[$path]; @@ -183,13 +179,13 @@ class AssetManager extends Component } if (is_file($src)) { - $dir = $this->hash($hashByName ? basename($src) : dirname($src) . filemtime($src)); + $dir = $this->hash(dirname($src) . filemtime($src)); $fileName = basename($src); $dstDir = $this->basePath . DIRECTORY_SEPARATOR . $dir; $dstFile = $dstDir . DIRECTORY_SEPARATOR . $fileName; if (!is_dir($dstDir)) { - @mkdir($dstDir, $this->newDirMode, true); + @mkdir($dstDir, $this->dirMode, true); } @@ -197,29 +193,26 @@ class AssetManager extends Component if (!is_file($dstFile)) { symlink($src, $dstFile); } - } elseif (@filemtime($dstFile) < @filemtime($src)) { + } elseif (@filemtime($dstFile) < @filemtime($src) || $forceCopy) { copy($src, $dstFile); - @chmod($dstFile, $this->newFileMode); + if ($this->fileMode !== null) { + @chmod($dstFile, $this->fileMode); + } } $url = $this->baseUrl . "/$dir/$fileName"; } else { - $dir = $this->hash($hashByName ? basename($src) : $src . filemtime($src)); + $dir = $this->hash($src . filemtime($src)); $dstDir = $this->basePath . DIRECTORY_SEPARATOR . $dir; - if ($this->linkAssets) { if (!is_dir($dstDir)) { symlink($src, $dstDir); } - } else { - if (!is_dir($dstDir) || $forceCopy) { - FileHelper::copyDirectory($src, $dstDir, array( - 'exclude' => $this->excludeFiles, - 'level' => $level, - 'newDirMode' => $this->newDirMode, - 'newFileMode' => $this->newFileMode, - )); - } + } elseif (!is_dir($dstDir) || $forceCopy) { + FileHelper::copyDirectory($src, $dstDir, array( + 'dirMode' => $this->dirMode, + 'fileMode' => $this->fileMode, + )); } $url = $this->baseUrl . '/' . $dir; @@ -232,20 +225,16 @@ class AssetManager extends Component * This method does not perform any publishing. It merely tells you * if the file or directory is published, where it will go. * @param string $path directory or file path being published - * @param boolean $hashByName whether the published directory should be named as the hashed basename. - * If false, the name will be the hash taken from dirname of the path being published and path mtime. - * Defaults to false. Set true if the path being published is shared among - * different extensions. * @return string the published file path. False if the file or directory does not exist */ - public function getPublishedPath($path, $hashByName = false) + public function getPublishedPath($path) { if (($path = realpath($path)) !== false) { $base = $this->basePath . DIRECTORY_SEPARATOR; if (is_file($path)) { - return $base . $this->hash($hashByName ? basename($path) : dirname($path) . filemtime($path)) . DIRECTORY_SEPARATOR . basename($path); + return $base . $this->hash(dirname($path) . filemtime($path)) . DIRECTORY_SEPARATOR . basename($path); } else { - return $base . $this->hash($hashByName ? basename($path) : $path . filemtime($path)); + return $base . $this->hash($path . filemtime($path)); } } else { return false; @@ -257,22 +246,18 @@ class AssetManager extends Component * This method does not perform any publishing. It merely tells you * if the file path is published, what the URL will be to access it. * @param string $path directory or file path being published - * @param boolean $hashByName whether the published directory should be named as the hashed basename. - * If false, the name will be the hash taken from dirname of the path being published and path mtime. - * Defaults to false. Set true if the path being published is shared among - * different extensions. * @return string the published URL for the file or directory. False if the file or directory does not exist. */ - public function getPublishedUrl($path, $hashByName = false) + public function getPublishedUrl($path) { if (isset($this->_published[$path])) { return $this->_published[$path]; } if (($path = realpath($path)) !== false) { if (is_file($path)) { - return $this->baseUrl . '/' . $this->hash($hashByName ? basename($path) : dirname($path) . filemtime($path)) . '/' . basename($path); + return $this->baseUrl . '/' . $this->hash(dirname($path) . filemtime($path)) . '/' . basename($path); } else { - return $this->baseUrl . '/' . $this->hash($hashByName ? basename($path) : $path . filemtime($path)); + return $this->baseUrl . '/' . $this->hash($path . filemtime($path)); } } else { return false; From 82535b254de8f0a65b7e74bfdca6a32b4d941034 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Wed, 17 Apr 2013 20:28:21 -0400 Subject: [PATCH 072/104] ... --- framework/web/AssetBundle.php | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/framework/web/AssetBundle.php b/framework/web/AssetBundle.php index a6b69d2..e811dc6 100644 --- a/framework/web/AssetBundle.php +++ b/framework/web/AssetBundle.php @@ -17,14 +17,21 @@ use yii\base\Object; class AssetBundle extends Object { /** - * @var string the root directory of the asset files. If this is not set, - * the assets are considered to be located under a Web-accessible folder already - * and no asset publishing will be performed. + * @var string the root directory of the source asset files. If this is set, + * the source asset files will be published to [[basePath]] when the bundle + * is being used the first time. + */ + public $sourcePath; + /** + * @var string the root directory of the public asset files. If this is not set + * while [[sourcePath]] is set, a default value will be set by [[AssetManager]] + * when it publishes the source asset files. If you set this property, please + * make sure the directory is Web accessible. */ public $basePath; /** * @var string the base URL that will be prefixed to the asset files. - * This property must be set if [[basePath]] is not set. + * This property must be set if you set [[basePath]] explicitly. * When this property is not set, it will be initialized as the base URL * that the assets are published to. */ @@ -33,12 +40,11 @@ class AssetBundle extends Object * @var array list of JavaScript files that this bundle contains. Each JavaScript file can * be specified in one of the three formats: * - * - a relative path: a path relative to [[basePath]] if [[basePath]] is set, - * or a URL relative to [[baseUrl]] if [[basePath]] is not set; + * - a relative file path: a path relative to [[basePath]];, * - an absolute URL; * - a path alias that can be resolved into a relative path or an absolute URL. * - * Note that you should not use backward slashes "\" to specify JavaScript files. + * Note that only forward slashes "/" should be used as directory separators. * * Each JavaScript file may be associated with options. In this case, the array key * should be the JavaScript file path, while the corresponding array value should @@ -49,12 +55,11 @@ class AssetBundle extends Object * @var array list of CSS files that this bundle contains. Each CSS file can * be specified in one of the three formats: * - * - a relative path: a path relative to [[basePath]] if [[basePath]] is set, - * or a URL relative to [[baseUrl]] if [[basePath]] is not set; + * - a relative file path: a path relative to [[basePath]];, * - an absolute URL; * - a path alias that can be resolved into a relative path or an absolute URL. * - * Note that you should not use backward slashes "\" to specify CSS files. + * Note that only forward slashes "/" should be used as directory separators. * * Each CSS file may be associated with options. In this case, the array key * should be the CSS file path, while the corresponding array value should @@ -71,8 +76,8 @@ class AssetBundle extends Object if ($this->baseUrl !== null) { $this->baseUrl = rtrim(Yii::getAlias($this->baseUrl), '/'); } - if ($this->basePath !== null) { - $this->basePath = rtrim(Yii::getAlias($this->basePath), '/\\'); + if ($this->sourcePath !== null) { + $this->sourcePath = rtrim(Yii::getAlias($this->sourcePath), '/\\'); } } @@ -105,8 +110,8 @@ class AssetBundle extends Object */ public function publish($assetManager) { - if ($this->basePath !== null) { - $baseUrl = $assetManager->publish($this->basePath); + if ($this->sourcePath !== null) { + $baseUrl = $assetManager->publish($this->sourcePath); if ($this->baseUrl === null) { $this->baseUrl = $baseUrl; } From db2392cdda65a061c543d704d1e793ef0d0d98af Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Thu, 18 Apr 2013 08:45:41 -0400 Subject: [PATCH 073/104] script WIP --- framework/base/ViewContent.php | 2 +- framework/web/AssetBundle.php | 122 +++++++++++++++++++++++++++-------------- framework/web/AssetManager.php | 17 ++---- framework/web/Sort.php | 2 +- 4 files changed, 87 insertions(+), 56 deletions(-) diff --git a/framework/base/ViewContent.php b/framework/base/ViewContent.php index a2951e7..dedd3bb 100644 --- a/framework/base/ViewContent.php +++ b/framework/base/ViewContent.php @@ -98,7 +98,7 @@ class ViewContent extends Component $bundle = $this->assetManager->getBundle($name); if ($bundle !== null) { $this->assetBundles[$name] = false; - $bundle->registerAssets($this); + $bundle->registerAssets($this, $this->assetManager); $this->assetBundles[$name] = true; } else { throw new InvalidConfigException("Unknown asset bundle: $name"); diff --git a/framework/web/AssetBundle.php b/framework/web/AssetBundle.php index e811dc6..066e248 100644 --- a/framework/web/AssetBundle.php +++ b/framework/web/AssetBundle.php @@ -8,6 +8,7 @@ namespace yii\web; use Yii; +use yii\base\InvalidConfigException; use yii\base\Object; /** @@ -17,34 +18,51 @@ use yii\base\Object; class AssetBundle extends Object { /** - * @var string the root directory of the source asset files. If this is set, - * the source asset files will be published to [[basePath]] when the bundle - * is being used the first time. + * @var string the root directory of the source asset files. A source asset file + * is a file that is part of your source code repository of your Web application. + * + * You must set this property if the directory containing the source asset files + * is not Web accessible (this is usually the case for extensions). + * + * By setting this property, the asset manager will publish the source asset files + * to a Web-accessible directory [[basePath]]. + * + * You can use either a directory or an alias of the directory. */ public $sourcePath; /** - * @var string the root directory of the public asset files. If this is not set - * while [[sourcePath]] is set, a default value will be set by [[AssetManager]] - * when it publishes the source asset files. If you set this property, please - * make sure the directory is Web accessible. + * @var string the Web-accessible directory that contains the asset files in this bundle. + * + * If [[sourcePath]] is set, this property will be *overwritten* by [[AssetManager]] + * when it publishes the asset files from [[sourcePath]]. + * + * If the bundle contains any assets that are specified in terms of relative file path, + * then this property must be set either manually or automatically (by asset manager via + * asset publishing). + * + * You can use either a directory or an alias of the directory. */ public $basePath; /** - * @var string the base URL that will be prefixed to the asset files. - * This property must be set if you set [[basePath]] explicitly. - * When this property is not set, it will be initialized as the base URL - * that the assets are published to. + * @var string the base URL that will be prefixed to the asset files for them to + * be accessed via Web server. + * + * If [[sourcePath]] is set, this property will be *overwritten* by [[AssetManager]] + * when it publishes the asset files from [[sourcePath]]. + * + * If the bundle contains any assets that are specified in terms of relative file path, + * then this property must be set either manually or automatically (by asset manager via + * asset publishing). + * + * You can use either a URL or an alias of the URL. */ public $baseUrl; /** * @var array list of JavaScript files that this bundle contains. Each JavaScript file can - * be specified in one of the three formats: - * - * - a relative file path: a path relative to [[basePath]];, - * - an absolute URL; - * - a path alias that can be resolved into a relative path or an absolute URL. + * be either a file path (without leading slash) relative to [[basePath]] or a URL representing + * an external JavaScript file. * - * Note that only forward slashes "/" should be used as directory separators. + * Note that only forward slash "/" can be used as directory separator. * * Each JavaScript file may be associated with options. In this case, the array key * should be the JavaScript file path, while the corresponding array value should @@ -53,13 +71,10 @@ class AssetBundle extends Object public $js = array(); /** * @var array list of CSS files that this bundle contains. Each CSS file can - * be specified in one of the three formats: + * be either a file path (without leading slash) relative to [[basePath]] or a URL representing + * an external CSS file. * - * - a relative file path: a path relative to [[basePath]];, - * - an absolute URL; - * - a path alias that can be resolved into a relative path or an absolute URL. - * - * Note that only forward slashes "/" should be used as directory separators. + * Note that only forward slash "/" can be used as directory separator. * * Each CSS file may be associated with options. In this case, the array key * should be the CSS file path, while the corresponding array value should @@ -71,50 +86,73 @@ class AssetBundle extends Object */ public $depends = array(); + /** + * Initializes the bundle. + */ public function init() { - if ($this->baseUrl !== null) { - $this->baseUrl = rtrim(Yii::getAlias($this->baseUrl), '/'); - } if ($this->sourcePath !== null) { $this->sourcePath = rtrim(Yii::getAlias($this->sourcePath), '/\\'); } + if ($this->basePath !== null) { + $this->basePath = rtrim(Yii::getAlias($this->basePath), '/\\'); + } + if ($this->baseUrl !== null) { + $this->baseUrl = rtrim(Yii::getAlias($this->baseUrl), '/'); + } } /** - * @param \yii\base\ViewContent $content + * @param \yii\base\ViewContent $page + * @param AssetManager $am + * @throws InvalidConfigException */ - public function registerAssets($content) + public function registerAssets($page, $am) { foreach ($this->depends as $name) { - $content->registerAssetBundle($name); + $page->registerAssetBundle($name); + } + + if ($this->sourcePath !== null) { + list ($this->basePath, $this->baseUrl) = $am->publish($this->sourcePath); } + foreach ($this->js as $js => $options) { $js = is_string($options) ? $options : $js; - if (strpos($js, '//') !== 0 && strpos($js, '://') === false) { - $js = $this->baseUrl . '/' . ltrim($js, '/'); + if (strpos($js, '/') !== 0 && strpos($js, '://') === false) { + if (isset($this->basePath, $this->baseUrl)) { + $js = $this->processAsset(ltrim($js, '/'), $this->basePath, $this->baseUrl); + } else { + throw new InvalidConfigException('Both of the "baseUrl" and "basePath" properties must be set.'); + } } - $content->registerJsFile($js, is_array($options) ? $options : array()); + $page->registerJsFile($js, is_array($options) ? $options : array()); } foreach ($this->css as $css => $options) { $css = is_string($options) ? $options : $css; if (strpos($css, '//') !== 0 && strpos($css, '://') === false) { - $css = $this->baseUrl . '/' . ltrim($css, '/'); + if (isset($this->basePath, $this->baseUrl)) { + $css = $this->processAsset(ltrim($css, '/'), $this->basePath, $this->baseUrl); + } else { + throw new InvalidConfigException('Both of the "baseUrl" and "basePath" properties must be set.'); + } } - $content->registerCssFile($css, is_array($options) ? $options : array()); + $page->registerCssFile($css, is_array($options) ? $options : array()); } } /** - * @param \yii\web\AssetManager $assetManager + * Processes the given asset file and returns a URL to the processed one. + * This method can be overwritten to support various types of asset files, such as LESS, Sass, TypeScript. + * + * Note that if the asset file is converted into another file, the new file must reside under the same + * directory as the given asset file. + * + * @param string $asset the asset file path to be processed. + * @return string the processed asset file path. */ - public function publish($assetManager) + protected function processAsset($asset, $basePath, $baseUrl) { - if ($this->sourcePath !== null) { - $baseUrl = $assetManager->publish($this->sourcePath); - if ($this->baseUrl === null) { - $this->baseUrl = $baseUrl; - } - } + return $this->baseUrl . '/' . $asset; } } \ No newline at end of file diff --git a/framework/web/AssetManager.php b/framework/web/AssetManager.php index c06d6b2..9a121d8 100644 --- a/framework/web/AssetManager.php +++ b/framework/web/AssetManager.php @@ -26,6 +26,7 @@ class AssetManager extends Component * may look for bundles declared in extensions. For more details, please refer to [[getBundle()]]. */ public $bundles; + public $bundleClass; /** * @return string the root directory storing the published asset files. */ @@ -97,11 +98,10 @@ class AssetManager extends Component /** * @param string $name - * @param boolean $publish * @return AssetBundle * @throws InvalidParamException */ - public function getBundle($name, $publish = true) + public function getBundle($name) { if (!isset($this->bundles[$name])) { $rootAlias = Yii::getRootAlias("@$name"); @@ -124,12 +124,6 @@ class AssetManager extends Component $this->bundles[$name] = Yii::createObject($config); } } - /** @var $bundle AssetBundle */ - $bundle = $this->bundles[$name]; - - if ($publish) { - $bundle->publish($this); - } return $this->bundles[$name]; } @@ -164,7 +158,7 @@ class AssetManager extends Component * in the target directory. This parameter is mainly useful during the development stage * when the original assets are being constantly changed. The consequence is that the performance * is degraded, which is not a concern during development, however. - * @return string an absolute URL to the published asset + * @return array the path (directory or file path) and the URL that the asset is published as. * @throws InvalidParamException if the asset to be published does not exist. */ public function publish($path, $forceCopy = false) @@ -200,7 +194,7 @@ class AssetManager extends Component } } - $url = $this->baseUrl . "/$dir/$fileName"; + return $this->_published[$path] = array($dstFile, $this->baseUrl . "/$dir/$fileName"); } else { $dir = $this->hash($src . filemtime($src)); $dstDir = $this->basePath . DIRECTORY_SEPARATOR . $dir; @@ -215,9 +209,8 @@ class AssetManager extends Component )); } - $url = $this->baseUrl . '/' . $dir; + return $this->_published[$path] = array($dstDir, $this->baseUrl . '/' . $dir); } - return $this->_published[$path] = $url; } /** diff --git a/framework/web/Sort.php b/framework/web/Sort.php index 7cfeeca..e5c2451 100644 --- a/framework/web/Sort.php +++ b/framework/web/Sort.php @@ -216,7 +216,7 @@ class Sort extends \yii\base\Object $url = $this->createUrl($attribute); - return Html::link($label, $url, $htmlOptions); + return Html::a($label, $url, $htmlOptions); } private $_attributeOrders; From a3d058574e6914de604c18c09e9a3c4966bee714 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Thu, 18 Apr 2013 11:26:39 -0400 Subject: [PATCH 074/104] refactored FileHelper::copyDirectory() --- framework/helpers/base/FileHelper.php | 33 ++++++++++++++++----------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/framework/helpers/base/FileHelper.php b/framework/helpers/base/FileHelper.php index d3804fb..478f978 100644 --- a/framework/helpers/base/FileHelper.php +++ b/framework/helpers/base/FileHelper.php @@ -124,7 +124,6 @@ class FileHelper return null; } - /** * Copies a whole directory as another one. * The files and sub-directories will also be copied over. @@ -134,15 +133,12 @@ class FileHelper * * - dirMode: integer, the permission to be set for newly copied directories. Defaults to 0777. * - fileMode: integer, the permission to be set for newly copied files. Defaults to the current environment setting. - * - filter: callback, a PHP callback that is called for every sub-directory and file to - * determine if it should be copied. The signature of the callback should be: - * - * ~~~ - * // $path is the file/directory path to be copied - * function ($path) { - * // return a boolean indicating if $path should be copied - * } - * ~~~ + * - beforeCopy: callback, a PHP callback that is called before copying each sub-directory or file. + * If the callback returns false, the copy operation for the sub-directory or file will be cancelled. + * The signature of the callback should be: `function ($from, $to)`, where `$from` is the sub-directory or + * file to be copied from, while `$to` is the copy target. + * - afterCopy: callback, a PHP callback that is called after a sub-directory or file is successfully copied. + * The signature of the callback is similar to that of `beforeCopy`. */ public static function copyDirectory($src, $dst, $options = array()) { @@ -155,16 +151,19 @@ class FileHelper if ($file === '.' || $file === '..') { continue; } - $srcPath = $src . DIRECTORY_SEPARATOR . $file; - if (!isset($options['filter']) || call_user_func($options['filter'], $srcPath)) { - $dstPath = $dst . DIRECTORY_SEPARATOR . $file; - if (is_file($srcPath)) { - copy($srcPath, $dstPath); + $from = $src . DIRECTORY_SEPARATOR . $file; + $to = $dst . DIRECTORY_SEPARATOR . $file; + if (!isset($options['beforeCopy']) || call_user_func($options['beforeCopy'], $from, $to)) { + if (is_file($from)) { + copy($from, $to); if (isset($options['fileMode'])) { - chmod($dstPath, $options['fileMode']); + chmod($to, $options['fileMode']); } } else { - static::copyDirectory($srcPath, $dstPath, $options); + static::copyDirectory($from, $to, $options); + } + if (isset($options['afterCopy'])) { + call_user_func($options['afterCopy'], $from, $to); } } } From 4db5041d66ab59a5cd8981d28e99f0a047da929f Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Thu, 18 Apr 2013 20:43:27 +0400 Subject: [PATCH 075/104] removed metions of 1.1 --- framework/web/AssetManager.php | 1 - framework/web/Response.php | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/framework/web/AssetManager.php b/framework/web/AssetManager.php index 9a121d8..55c54d4 100644 --- a/framework/web/AssetManager.php +++ b/framework/web/AssetManager.php @@ -56,7 +56,6 @@ class AssetManager extends Component /** * @var array list of directories and files which should be excluded from the publishing process. * Defaults to exclude '.svn' and '.gitignore' files only. This option has no effect if {@link linkAssets} is enabled. - * @since 1.1.6 **/ public $excludeFiles = array('.svn', '.gitignore'); /** diff --git a/framework/web/Response.php b/framework/web/Response.php index da2482f..1d604e9 100644 --- a/framework/web/Response.php +++ b/framework/web/Response.php @@ -112,8 +112,8 @@ class Response extends \yii\base\Response *
  • mimeType: mime type of the file, if not set it will be guessed automatically based on the file name, if set to null no content-type header will be sent.
  • *
  • xHeader: appropriate x-sendfile header, defaults to "X-Sendfile"
  • *
  • terminate: whether to terminate the current application after calling this method, defaults to true
  • - *
  • forceDownload: specifies whether the file will be downloaded or shown inline, defaults to true. (Since version 1.1.9.)
  • - *
  • addHeaders: an array of additional http headers in header-value pairs (available since version 1.1.10)
  • + *
  • forceDownload: specifies whether the file will be downloaded or shown inline, defaults to true
  • + *
  • addHeaders: an array of additional http headers in header-value pairs
  • * */ public function xSendFile($filePath, $options = array()) From c006ebeb4900fa9b03bb5b4828c83b8d161c2cf7 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Thu, 18 Apr 2013 20:46:37 +0400 Subject: [PATCH 076/104] added .hgignore to list of typically ignored files --- framework/console/controllers/AppController.php | 4 ++-- framework/web/AssetManager.php | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/framework/console/controllers/AppController.php b/framework/console/controllers/AppController.php index 237ba0f..a47acfe 100644 --- a/framework/console/controllers/AppController.php +++ b/framework/console/controllers/AppController.php @@ -294,7 +294,7 @@ class AppController extends Controller * @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. + * be ignored in list building process. * @param array $renameMap hash array of file names that should be * renamed. Example value: array('1.old.txt'=>'2.new.txt'). * @return array the file list (see {@link copyFiles}) @@ -304,7 +304,7 @@ class AppController extends Controller $list = array(); $handle = opendir($sourceDir); while(($file = readdir($handle)) !== false) { - if(in_array($file, array('.', '..', '.svn', '.gitignore')) || in_array($file, $ignoreFiles)) { + if(in_array($file, array('.', '..', '.svn', '.gitignore', '.hgignore')) || in_array($file, $ignoreFiles)) { continue; } $sourcePath = $sourceDir.DIRECTORY_SEPARATOR.$file; diff --git a/framework/web/AssetManager.php b/framework/web/AssetManager.php index 55c54d4..8788253 100644 --- a/framework/web/AssetManager.php +++ b/framework/web/AssetManager.php @@ -55,9 +55,9 @@ class AssetManager extends Component public $linkAssets = false; /** * @var array list of directories and files which should be excluded from the publishing process. - * Defaults to exclude '.svn' and '.gitignore' files only. This option has no effect if {@link linkAssets} is enabled. + * Defaults to exclude '.svn', '.gitignore' and '.hgignore' files only. This option has no effect if {@link linkAssets} is enabled. **/ - public $excludeFiles = array('.svn', '.gitignore'); + public $excludeFiles = array('.svn', '.gitignore', '.hgignore'); /** * @var integer the permission to be set for newly published asset files. * This value will be used by PHP chmod() function. From fadb528f2e367d0eff289173567bb6b4a7f0bfef Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Thu, 18 Apr 2013 12:58:45 -0400 Subject: [PATCH 077/104] script WIP --- framework/helpers/base/FileHelper.php | 2 +- framework/web/AssetBundle.php | 19 +------- framework/web/AssetManager.php | 86 +++++++++++++++++++++++++---------- 3 files changed, 64 insertions(+), 43 deletions(-) diff --git a/framework/helpers/base/FileHelper.php b/framework/helpers/base/FileHelper.php index 478f978..2f62f43 100644 --- a/framework/helpers/base/FileHelper.php +++ b/framework/helpers/base/FileHelper.php @@ -157,7 +157,7 @@ class FileHelper if (is_file($from)) { copy($from, $to); if (isset($options['fileMode'])) { - chmod($to, $options['fileMode']); + @chmod($to, $options['fileMode']); } } else { static::copyDirectory($from, $to, $options); diff --git a/framework/web/AssetBundle.php b/framework/web/AssetBundle.php index 066e248..5b04637 100644 --- a/framework/web/AssetBundle.php +++ b/framework/web/AssetBundle.php @@ -121,7 +121,7 @@ class AssetBundle extends Object $js = is_string($options) ? $options : $js; if (strpos($js, '/') !== 0 && strpos($js, '://') === false) { if (isset($this->basePath, $this->baseUrl)) { - $js = $this->processAsset(ltrim($js, '/'), $this->basePath, $this->baseUrl); + $js = $am->processAsset(ltrim($js, '/'), $this->basePath, $this->baseUrl); } else { throw new InvalidConfigException('Both of the "baseUrl" and "basePath" properties must be set.'); } @@ -132,7 +132,7 @@ class AssetBundle extends Object $css = is_string($options) ? $options : $css; if (strpos($css, '//') !== 0 && strpos($css, '://') === false) { if (isset($this->basePath, $this->baseUrl)) { - $css = $this->processAsset(ltrim($css, '/'), $this->basePath, $this->baseUrl); + $css = $am->processAsset(ltrim($css, '/'), $this->basePath, $this->baseUrl); } else { throw new InvalidConfigException('Both of the "baseUrl" and "basePath" properties must be set.'); } @@ -140,19 +140,4 @@ class AssetBundle extends Object $page->registerCssFile($css, is_array($options) ? $options : array()); } } - - /** - * Processes the given asset file and returns a URL to the processed one. - * This method can be overwritten to support various types of asset files, such as LESS, Sass, TypeScript. - * - * Note that if the asset file is converted into another file, the new file must reside under the same - * directory as the given asset file. - * - * @param string $asset the asset file path to be processed. - * @return string the processed asset file path. - */ - protected function processAsset($asset, $basePath, $baseUrl) - { - return $this->baseUrl . '/' . $asset; - } } \ No newline at end of file diff --git a/framework/web/AssetManager.php b/framework/web/AssetManager.php index 9a121d8..30f4c11 100644 --- a/framework/web/AssetManager.php +++ b/framework/web/AssetManager.php @@ -21,12 +21,10 @@ use yii\helpers\FileHelper; class AssetManager extends Component { /** - * @var array list of asset bundles. The keys are the bundle names, and the values are the configuration - * arrays for creating [[AssetBundle]] objects. Besides the bundles listed here, the asset manager - * may look for bundles declared in extensions. For more details, please refer to [[getBundle()]]. + * @var array list of available asset bundles. The keys are the bundle names, and the values are the configuration + * arrays for creating the [[AssetBundle]] objects. */ public $bundles; - public $bundleClass; /** * @return string the root directory storing the published asset files. */ @@ -54,12 +52,6 @@ class AssetManager extends Component */ public $linkAssets = false; /** - * @var array list of directories and files which should be excluded from the publishing process. - * Defaults to exclude '.svn' and '.gitignore' files only. This option has no effect if {@link linkAssets} is enabled. - * @since 1.1.6 - **/ - public $excludeFiles = array('.svn', '.gitignore'); - /** * @var integer the permission to be set for newly published asset files. * This value will be used by PHP chmod() function. * If not set, the permission will be determined by the current environment. @@ -97,9 +89,21 @@ class AssetManager extends Component } /** - * @param string $name - * @return AssetBundle - * @throws InvalidParamException + * Returns the named bundle. + * This method will first look for the bundle in [[bundles]]. If not found, + * it will attempt to find the bundle from an installed extension using the following procedure: + * + * 1. Convert the bundle into a path alias; + * 2. Determine the root alias and use it to locate the bundle manifest file "assets.php"; + * 3. Look for the bundle in the manifest file. + * + * For example, given the bundle name "foo/button", the method will first convert it + * into the path alias "@foo/button"; since "@foo" is the root alias, it will look + * for the bundle manifest file "@foo/assets.php". The manifest file should declare + * the bundles used by the "foo/button" extension. + * + * @param string $name the bundle name + * @return AssetBundle the loaded bundle object. Null is returned if the bundle does not exist. */ public function getBundle($name) { @@ -114,7 +118,7 @@ class AssetManager extends Component } } if (!isset($this->bundles[$name])) { - throw new InvalidParamException("Unable to find the asset bundle: $name"); + return null; } } if (is_array($this->bundles[$name])) { @@ -129,6 +133,20 @@ class AssetManager extends Component } /** + * Processes the given asset file and returns a URL to the processed one. + * This method can be overwritten to support various types of asset files, such as LESS, Sass, TypeScript. + * @param string $asset the asset file path to be processed. The file path is relative + * to $basePath, and it may contain forward slashes to indicate sub-directories (e.g. "js/main.js"). + * @param string $basePath the directory that contains the asset file. + * @param string $baseUrl the corresponding URL of $basePath. + * @return string the processed asset file path. + */ + public function processAsset($asset, $basePath, $baseUrl) + { + return $baseUrl . '/' . $asset; + } + + /** * @var array published assets */ private $_published = array(); @@ -154,14 +172,26 @@ class AssetManager extends Component * discussion: http://code.google.com/p/yii/issues/detail?id=2579 * * @param string $path the asset (file or directory) to be published - * @param boolean $forceCopy whether the asset should ALWAYS be copied even if it is found - * in the target directory. This parameter is mainly useful during the development stage - * when the original assets are being constantly changed. The consequence is that the performance - * is degraded, which is not a concern during development, however. + * @param array $options the options to be applied when publishing a directory. + * The following options are supported: + * + * - beforeCopy: callback, a PHP callback that is called before copying each sub-directory or file. + * This option is used only when publishing a directory. If the callback returns false, the copy + * operation for the sub-directory or file will be cancelled. + * The signature of the callback should be: `function ($from, $to)`, where `$from` is the sub-directory or + * file to be copied from, while `$to` is the copy target. + * - afterCopy: callback, a PHP callback that is called after a sub-directory or file is successfully copied. + * This option is used only when publishing a directory. The signature of the callback is similar to that + * of `beforeCopy`. + * - forceCopy: boolean, whether the directory being published should be copied even if + * it is found in the target directory. This option is used only when publishing a directory. + * You may want to set this to be true during the development stage to make sure the published + * directory is always up-to-date. Do not set this to true on production servers as it will + * significantly degrade the performance. * @return array the path (directory or file path) and the URL that the asset is published as. * @throws InvalidParamException if the asset to be published does not exist. */ - public function publish($path, $forceCopy = false) + public function publish($path, $options = array()) { if (isset($this->_published[$path])) { return $this->_published[$path]; @@ -179,15 +209,14 @@ class AssetManager extends Component $dstFile = $dstDir . DIRECTORY_SEPARATOR . $fileName; if (!is_dir($dstDir)) { - @mkdir($dstDir, $this->dirMode, true); + mkdir($dstDir, $this->dirMode, true); } - if ($this->linkAssets) { if (!is_file($dstFile)) { symlink($src, $dstFile); } - } elseif (@filemtime($dstFile) < @filemtime($src) || $forceCopy) { + } elseif (@filemtime($dstFile) < @filemtime($src)) { copy($src, $dstFile); if ($this->fileMode !== null) { @chmod($dstFile, $this->fileMode); @@ -202,11 +231,18 @@ class AssetManager extends Component if (!is_dir($dstDir)) { symlink($src, $dstDir); } - } elseif (!is_dir($dstDir) || $forceCopy) { - FileHelper::copyDirectory($src, $dstDir, array( + } elseif (!is_dir($dstDir) || !empty($options['forceCopy'])) { + $opts = array( 'dirMode' => $this->dirMode, 'fileMode' => $this->fileMode, - )); + ); + if (isset($options['beforeCopy'])) { + $opts['beforeCopy'] = $options['beforeCopy']; + } + if (isset($options['afterCopy'])) { + $opts['afterCopy'] = $options['afterCopy']; + } + FileHelper::copyDirectory($src, $dstDir, $opts); } return $this->_published[$path] = array($dstDir, $this->baseUrl . '/' . $dir); From bab3222907764537687d0ca1cc717e33fb9acd9c Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Thu, 18 Apr 2013 15:29:30 -0400 Subject: [PATCH 078/104] Added doc. --- framework/web/AssetBundle.php | 7 ++++++- framework/web/AssetManager.php | 4 ++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/framework/web/AssetBundle.php b/framework/web/AssetBundle.php index 5b04637..76d901f 100644 --- a/framework/web/AssetBundle.php +++ b/framework/web/AssetBundle.php @@ -85,6 +85,11 @@ class AssetBundle extends Object * @var array list of the bundle names that this bundle depends on */ public $depends = array(); + /** + * @var array the options to be passed to [[AssetManager::publish()]] when the asset bundle + * is being published. + */ + public $publishOption = array(); /** * Initializes the bundle. @@ -114,7 +119,7 @@ class AssetBundle extends Object } if ($this->sourcePath !== null) { - list ($this->basePath, $this->baseUrl) = $am->publish($this->sourcePath); + list ($this->basePath, $this->baseUrl) = $am->publish($this->sourcePath, $this->publishOption); } foreach ($this->js as $js => $options) { diff --git a/framework/web/AssetManager.php b/framework/web/AssetManager.php index 9f01173..5375d08 100644 --- a/framework/web/AssetManager.php +++ b/framework/web/AssetManager.php @@ -164,6 +164,10 @@ class AssetManager extends Component * Note, in case $forceCopy is false the method only checks the existence of the target * directory to avoid repetitive copying (which is very expensive). * + * By default, when publishing a directory, subdirectories and files whose name starts with a dot "." + * will NOT be published. If you want to change this behavior, you may specify the "beforeCopy" option + * as explained in the `$options` parameter. + * * Note: On rare scenario, a race condition can develop that will lead to a * one-time-manifestation of a non-critical problem in the creation of the directory * that holds the published assets. This problem can be avoided altogether by 'requesting' From d22e8ea34b589830d896d703396b3912fdf186b7 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Thu, 18 Apr 2013 15:46:01 -0400 Subject: [PATCH 079/104] Added YiiBase::importNamespaces(). --- framework/YiiBase.php | 24 ++++++++++++++++++++++++ framework/base/Application.php | 12 ------------ framework/web/AssetBundle.php | 4 ++-- 3 files changed, 26 insertions(+), 14 deletions(-) diff --git a/framework/YiiBase.php b/framework/YiiBase.php index c32cc28..9d501b1 100644 --- a/framework/YiiBase.php +++ b/framework/YiiBase.php @@ -139,6 +139,30 @@ class YiiBase } /** + * Imports a set of namespaces. + * + * By importing a namespace, the method will create an alias for the directory corresponding + * to the namespace. For example, if "foo\bar" is a namespace associated with the directory + * "path/to/foo/bar", then an alias "@foo/bar" will be created for this directory. + * + * This method is typically invoked in the bootstrap file to import the namespaces of + * the installed extensions. By default, Composer, when installing new extensions, will + * generate such a mapping file which can be loaded and passed to this method. + * + * @param array $namespaces the namespaces to be imported. The keys are the namespaces, + * and the values are the corresponding directories. + */ + public static function importNamespaces($namespaces) + { + foreach ($namespaces as $name => $path) { + if ($name !== '') { + $name = '@' . str_replace('\\', '/', $name); + static::setAlias($name, $path); + } + } + } + + /** * Translates a path alias into an actual path. * * The translation is done according to the following procedure: diff --git a/framework/base/Application.php b/framework/base/Application.php index e0d0237..88dfcb9 100644 --- a/framework/base/Application.php +++ b/framework/base/Application.php @@ -56,11 +56,6 @@ class Application extends Module * If this is false, layout will be disabled. */ public $layout = 'main'; - /** - * @var array list of installed extensions. The array keys are the extension names, and the array - * values are the corresponding extension root source directories or path aliases. - */ - public $extensions = array(); private $_ended = false; @@ -92,13 +87,6 @@ class Application extends Module throw new InvalidConfigException('The "basePath" configuration is required.'); } - if (isset($config['extensions'])) { - foreach ($config['extensions'] as $name => $path) { - Yii::setAlias("@$name", $path); - } - unset($config['extensions']); - } - $this->registerErrorHandlers(); $this->registerCoreComponents(); diff --git a/framework/web/AssetBundle.php b/framework/web/AssetBundle.php index 76d901f..124f5e6 100644 --- a/framework/web/AssetBundle.php +++ b/framework/web/AssetBundle.php @@ -89,7 +89,7 @@ class AssetBundle extends Object * @var array the options to be passed to [[AssetManager::publish()]] when the asset bundle * is being published. */ - public $publishOption = array(); + public $publishOptions = array(); /** * Initializes the bundle. @@ -119,7 +119,7 @@ class AssetBundle extends Object } if ($this->sourcePath !== null) { - list ($this->basePath, $this->baseUrl) = $am->publish($this->sourcePath, $this->publishOption); + list ($this->basePath, $this->baseUrl) = $am->publish($this->sourcePath, $this->publishOptions); } foreach ($this->js as $js => $options) { From 1f47ea9781fd85dc08bcae78da924c6996f12e90 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Thu, 18 Apr 2013 15:55:28 -0400 Subject: [PATCH 080/104] Added asset processor concept. --- framework/web/AssetManager.php | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/framework/web/AssetManager.php b/framework/web/AssetManager.php index 5375d08..72dd06b 100644 --- a/framework/web/AssetManager.php +++ b/framework/web/AssetManager.php @@ -26,6 +26,13 @@ class AssetManager extends Component */ public $bundles; /** + * @var array list of asset processors. An asset processor will convert a special type of asset files + * (e.g. LESS, Sass, TypeScript) into JS or CSS files. The array keys are the file extension names + * (e.g. "less", "sass", "ts"), and the array values are the corresponding configuration arrays + * for creating the processor objects. + */ + public $processors; + /** * @return string the root directory storing the published asset files. */ public $basePath = '@wwwroot/assets'; @@ -143,7 +150,15 @@ class AssetManager extends Component */ public function processAsset($asset, $basePath, $baseUrl) { - return $baseUrl . '/' . $asset; + $ext = pathinfo($asset, PATHINFO_EXTENSION); + if (isset($this->processors[$ext])) { + if (is_array($this->processors[$ext])) { + $this->processors[$ext] = Yii::createObject($this->processors[$ext]); + } + return $this->processors[$ext]->process($asset, $basePath, $baseUrl); + } else { + return $baseUrl . '/' . $asset; + } } /** From 990489354d3690193a0ace75d0979e1289ee2a42 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Thu, 18 Apr 2013 23:41:46 +0400 Subject: [PATCH 081/104] corrected exception class --- framework/base/Module.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework/base/Module.php b/framework/base/Module.php index ee97614..d99778d 100644 --- a/framework/base/Module.php +++ b/framework/base/Module.php @@ -207,7 +207,7 @@ abstract class Module extends Component * Sets the root directory of the module. * This method can only be invoked at the beginning of the constructor. * @param string $path the root directory of the module. This can be either a directory name or a path alias. - * @throws Exception if the directory does not exist. + * @throws InvalidParamException if the directory does not exist. */ public function setBasePath($path) { From 77233cf39edae7ea727b998fd5b4705366273fae Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Fri, 19 Apr 2013 00:38:42 +0400 Subject: [PATCH 082/104] removed dev comment --- framework/base/Application.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/framework/base/Application.php b/framework/base/Application.php index 88dfcb9..027dec8 100644 --- a/framework/base/Application.php +++ b/framework/base/Application.php @@ -60,8 +60,7 @@ class Application extends Module private $_ended = false; /** - * @var string Used to reserve memory for fatal error handler. This memory - * reserve can be removed if it's OK to write to PHP log only in this particular case. + * @var string Used to reserve memory for fatal error handler. */ private $_memoryReserve; From a701697c96e58ce6c15b9e17bd16a8f7ac5d0092 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Fri, 19 Apr 2013 01:58:23 +0400 Subject: [PATCH 083/104] fixed component name --- framework/web/Application.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework/web/Application.php b/framework/web/Application.php index 32f6479..90f7292 100644 --- a/framework/web/Application.php +++ b/framework/web/Application.php @@ -103,7 +103,7 @@ class Application extends \yii\base\Application */ public function getAssets() { - return $this->getComponent('user'); + return $this->getComponent('assets'); } /** From 90b2a54f2de2a7967f4b2abeb3c54a17b34fb917 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Fri, 19 Apr 2013 02:30:46 +0400 Subject: [PATCH 084/104] updated default app template --- framework/console/webapp/default/index.php | 8 ++++---- framework/console/webapp/default/protected/config/main.php | 4 ++++ framework/console/webapp/default/protected/views/layouts/main.php | 7 ++++--- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/framework/console/webapp/default/index.php b/framework/console/webapp/default/index.php index 461b364..b84e257 100644 --- a/framework/console/webapp/default/index.php +++ b/framework/console/webapp/default/index.php @@ -1,10 +1,10 @@ run(); \ No newline at end of file diff --git a/framework/console/webapp/default/protected/config/main.php b/framework/console/webapp/default/protected/config/main.php index 1e3f981..795811e 100644 --- a/framework/console/webapp/default/protected/config/main.php +++ b/framework/console/webapp/default/protected/config/main.php @@ -1,5 +1,6 @@ 'webapp', 'name' => 'My Web Application', 'components' => array( @@ -12,5 +13,8 @@ return array( 'password' => '', ), */ + 'cache' => array( + 'class' => 'yii\caching\DummyCache', + ), ), ); \ No newline at end of file diff --git a/framework/console/webapp/default/protected/views/layouts/main.php b/framework/console/webapp/default/protected/views/layouts/main.php index 197b4a2..a4215dc 100644 --- a/framework/console/webapp/default/protected/views/layouts/main.php +++ b/framework/console/webapp/default/protected/views/layouts/main.php @@ -1,11 +1,12 @@ + - + - <?php echo $this->context->pageTitle?> + <?php echo Html::encode($this->page->title)?> -

    context->pageTitle?>

    +

    page->title)?>

    From c32def86184f2a5128bf8224c485f7b75004aaf7 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Thu, 18 Apr 2013 18:59:27 -0400 Subject: [PATCH 085/104] turn asset manager into a getter. --- framework/base/ViewContent.php | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/framework/base/ViewContent.php b/framework/base/ViewContent.php index dedd3bb..e5a1a06 100644 --- a/framework/base/ViewContent.php +++ b/framework/base/ViewContent.php @@ -24,11 +24,6 @@ class ViewContent extends Component const TOKEN_BODY_BEGIN = ''; const TOKEN_BODY_END = ''; - /** - * @var \yii\web\AssetManager - */ - public $assetManager; - public $assetBundles; public $title; public $metaTags; @@ -42,14 +37,6 @@ class ViewContent extends Component public $jsInBody; public $jsFilesInBody; - public function init() - { - parent::init(); - if ($this->assetManager === null) { - $this->assetManager = Yii::$app->getAssets(); - } - } - public function reset() { $this->title = null; @@ -64,6 +51,21 @@ class ViewContent extends Component $this->jsInBody = null; $this->jsFilesInBody = null; } + + private $_assetManager; + + /** + * @return \yii\web\AssetManager + */ + public function getAssetManager() + { + return $this->_assetManager ?: Yii::$app->getAssets(); + } + + public function setAssetManager($value) + { + $this->_assetManager = $value; + } public function begin() { @@ -95,10 +97,11 @@ class ViewContent extends Component public function registerAssetBundle($name) { if (!isset($this->assetBundles[$name])) { - $bundle = $this->assetManager->getBundle($name); + $am = $this->getAssetManager(); + $bundle = $am->getBundle($name); if ($bundle !== null) { $this->assetBundles[$name] = false; - $bundle->registerAssets($this, $this->assetManager); + $bundle->registerAssets($this, $am); $this->assetBundles[$name] = true; } else { throw new InvalidConfigException("Unknown asset bundle: $name"); From 2a12fdbcdf35f5cbc678d0d9beddae6f382203db Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Fri, 19 Apr 2013 03:51:08 +0400 Subject: [PATCH 086/104] safer exception rendering --- framework/base/ErrorHandler.php | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/framework/base/ErrorHandler.php b/framework/base/ErrorHandler.php index dc83474..91d5982 100644 --- a/framework/base/ErrorHandler.php +++ b/framework/base/ErrorHandler.php @@ -81,15 +81,20 @@ 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; - if (!YII_DEBUG || $exception instanceof UserException) { - $viewName = $this->errorView; - } else { - $viewName = $this->exceptionView; + try { + $view = new View; + if (!YII_DEBUG || $exception instanceof UserException) { + $viewName = $this->errorView; + } else { + $viewName = $this->exceptionView; + } + echo $view->renderFile($viewName, array( + 'exception' => $exception, + ), $this); + } + catch (\Exception $e) { + \Yii::$app->renderException($e); } - echo $view->renderFile($viewName, array( - 'exception' => $exception, - ), $this); } } else { \Yii::$app->renderException($exception); From 767a78f8b8925563cd5c11e115813e84c7387f2a Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Fri, 19 Apr 2013 04:12:30 +0400 Subject: [PATCH 087/104] better handling of errors during rendering an error --- framework/base/Application.php | 2 +- framework/base/ErrorHandler.php | 27 +++++++++++++++------------ 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/framework/base/Application.php b/framework/base/Application.php index 027dec8..c498a8e 100644 --- a/framework/base/Application.php +++ b/framework/base/Application.php @@ -411,7 +411,7 @@ class Application extends Module error_log($exception); if (($handler = $this->getErrorHandler()) !== null) { - @$handler->handle($exception); + $handler->handle($exception); } else { $this->renderException($exception); } diff --git a/framework/base/ErrorHandler.php b/framework/base/ErrorHandler.php index 91d5982..f9818ac 100644 --- a/framework/base/ErrorHandler.php +++ b/framework/base/ErrorHandler.php @@ -81,19 +81,22 @@ class ErrorHandler extends Component if (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] === 'XMLHttpRequest') { \Yii::$app->renderException($exception); } else { - try { - $view = new View; - if (!YII_DEBUG || $exception instanceof UserException) { - $viewName = $this->errorView; - } else { - $viewName = $this->exceptionView; - } - echo $view->renderFile($viewName, array( - 'exception' => $exception, - ), $this); + if(YII_DEBUG) { + ini_set('display_errors', 1); } - catch (\Exception $e) { - \Yii::$app->renderException($e); + + $view = new View; + if (!YII_DEBUG || $exception instanceof UserException) { + $viewName = $this->errorView; + } else { + $viewName = $this->exceptionView; + } + echo $view->renderFile($viewName, array( + 'exception' => $exception, + ), $this); + + if(YII_DEBUG) { + ini_set('display_errors', 0); } } } else { From 0a41b006ee319b3c3c27eef469faf94371dbe913 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Fri, 19 Apr 2013 04:22:36 +0400 Subject: [PATCH 088/104] removed unnecessary code, added comment about displaying errors --- framework/base/ErrorHandler.php | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/framework/base/ErrorHandler.php b/framework/base/ErrorHandler.php index f9818ac..98a061d 100644 --- a/framework/base/ErrorHandler.php +++ b/framework/base/ErrorHandler.php @@ -81,6 +81,8 @@ class ErrorHandler extends Component if (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] === 'XMLHttpRequest') { \Yii::$app->renderException($exception); } else { + // if there is an error during error rendering it's useful to + // display PHP error in debug mode instead of a blank screen if(YII_DEBUG) { ini_set('display_errors', 1); } @@ -94,10 +96,6 @@ class ErrorHandler extends Component echo $view->renderFile($viewName, array( 'exception' => $exception, ), $this); - - if(YII_DEBUG) { - ini_set('display_errors', 0); - } } } else { \Yii::$app->renderException($exception); From 30e6d28b7746992b651a21111d452ee084a372d0 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Thu, 18 Apr 2013 23:22:59 -0400 Subject: [PATCH 089/104] Finished asset converter. --- framework/base/ViewContent.php | 1 + framework/web/AssetBundle.php | 6 ++++-- framework/web/AssetConverter.php | 45 +++++++++++++++++++++++++++++++++++++++ framework/web/AssetManager.php | 39 ++++++++++++++------------------- framework/web/IAssetConverter.php | 17 +++++++++++++++ 5 files changed, 83 insertions(+), 25 deletions(-) create mode 100644 framework/web/AssetConverter.php create mode 100644 framework/web/IAssetConverter.php diff --git a/framework/base/ViewContent.php b/framework/base/ViewContent.php index e5a1a06..c7a3bc4 100644 --- a/framework/base/ViewContent.php +++ b/framework/base/ViewContent.php @@ -39,6 +39,7 @@ class ViewContent extends Component public function reset() { + $this->assetBundles = null; $this->title = null; $this->metaTags = null; $this->linkTags = null; diff --git a/framework/web/AssetBundle.php b/framework/web/AssetBundle.php index 124f5e6..f82173b 100644 --- a/framework/web/AssetBundle.php +++ b/framework/web/AssetBundle.php @@ -122,11 +122,13 @@ class AssetBundle extends Object list ($this->basePath, $this->baseUrl) = $am->publish($this->sourcePath, $this->publishOptions); } + $converter = $am->getConverter(); + foreach ($this->js as $js => $options) { $js = is_string($options) ? $options : $js; if (strpos($js, '/') !== 0 && strpos($js, '://') === false) { if (isset($this->basePath, $this->baseUrl)) { - $js = $am->processAsset(ltrim($js, '/'), $this->basePath, $this->baseUrl); + $js = $converter->convert(ltrim($js, '/'), $this->basePath, $this->baseUrl); } else { throw new InvalidConfigException('Both of the "baseUrl" and "basePath" properties must be set.'); } @@ -137,7 +139,7 @@ class AssetBundle extends Object $css = is_string($options) ? $options : $css; if (strpos($css, '//') !== 0 && strpos($css, '://') === false) { if (isset($this->basePath, $this->baseUrl)) { - $css = $am->processAsset(ltrim($css, '/'), $this->basePath, $this->baseUrl); + $css = $converter->convert(ltrim($css, '/'), $this->basePath, $this->baseUrl); } else { throw new InvalidConfigException('Both of the "baseUrl" and "basePath" properties must be set.'); } diff --git a/framework/web/AssetConverter.php b/framework/web/AssetConverter.php new file mode 100644 index 0000000..26cc59b --- /dev/null +++ b/framework/web/AssetConverter.php @@ -0,0 +1,45 @@ + + * @since 2.0 + */ +class AssetConverter extends Component implements IAssetConverter +{ + public $commands = array( + 'less' => array('css', 'lessc %s %s'), + 'scss' => array('css', 'sass %s %s'), + 'sass' => array('css', 'sass %s %s'), + 'styl' => array('js', 'stylus < %s > %s'), + ); + + public function convert($asset, $basePath, $baseUrl) + { + $pos = strrpos($asset, '.'); + if ($pos !== false) { + $ext = substr($asset, $pos + 1); + if (isset($this->commands[$ext])) { + list ($ext, $command) = $this->commands[$ext]; + $result = substr($asset, 0, $pos + 1) . $ext; + if (@filemtime("$basePath/$result") < filemtime("$basePath/$asset")) { + $output = array(); + $command = sprintf($command, "$basePath/$asset", "$basePath/$result"); + exec($command, $output); + Yii::info("Converted $asset into $result: " . implode("\n", $output), __METHOD__); + return "$baseUrl/$result"; + } + } + } + return "$baseUrl/$asset"; + } +} \ No newline at end of file diff --git a/framework/web/AssetManager.php b/framework/web/AssetManager.php index 72dd06b..c4ad385 100644 --- a/framework/web/AssetManager.php +++ b/framework/web/AssetManager.php @@ -26,13 +26,6 @@ class AssetManager extends Component */ public $bundles; /** - * @var array list of asset processors. An asset processor will convert a special type of asset files - * (e.g. LESS, Sass, TypeScript) into JS or CSS files. The array keys are the file extension names - * (e.g. "less", "sass", "ts"), and the array values are the corresponding configuration arrays - * for creating the processor objects. - */ - public $processors; - /** * @return string the root directory storing the published asset files. */ public $basePath = '@wwwroot/assets'; @@ -139,26 +132,26 @@ class AssetManager extends Component return $this->bundles[$name]; } + private $_converter; + /** - * Processes the given asset file and returns a URL to the processed one. - * This method can be overwritten to support various types of asset files, such as LESS, Sass, TypeScript. - * @param string $asset the asset file path to be processed. The file path is relative - * to $basePath, and it may contain forward slashes to indicate sub-directories (e.g. "js/main.js"). - * @param string $basePath the directory that contains the asset file. - * @param string $baseUrl the corresponding URL of $basePath. - * @return string the processed asset file path. + * @return IAssetConverter */ - public function processAsset($asset, $basePath, $baseUrl) + public function getConverter() { - $ext = pathinfo($asset, PATHINFO_EXTENSION); - if (isset($this->processors[$ext])) { - if (is_array($this->processors[$ext])) { - $this->processors[$ext] = Yii::createObject($this->processors[$ext]); - } - return $this->processors[$ext]->process($asset, $basePath, $baseUrl); - } else { - return $baseUrl . '/' . $asset; + if ($this->_converter === null) { + $this->_converter = Yii::createObject(array( + 'class' => 'yii\\web\\AssetConverter', + )); + } elseif (is_array($this->_converter) || is_string($this->_converter)) { + $this->_converter = Yii::createObject($this->_converter); } + return $this->_converter; + } + + public function setConverter($value) + { + $this->_converter = $value; } /** diff --git a/framework/web/IAssetConverter.php b/framework/web/IAssetConverter.php new file mode 100644 index 0000000..994cb2f --- /dev/null +++ b/framework/web/IAssetConverter.php @@ -0,0 +1,17 @@ + + * @since 2.0 + */ +interface IAssetConverter +{ + public function convert($asset, $basePath, $baseUrl); +} \ No newline at end of file From 41ea94853bb73f67f1bbf16a855c3fb3f14fbc1a Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Fri, 19 Apr 2013 15:19:29 -0400 Subject: [PATCH 090/104] refactoring and documentation for asset/script management. --- framework/base/View.php | 366 +++++++++++++++++++++++++++++++++++++++-- framework/base/ViewContent.php | 235 -------------------------- framework/web/Application.php | 6 +- framework/web/AssetBundle.php | 38 +++-- framework/web/AssetManager.php | 10 +- 5 files changed, 388 insertions(+), 267 deletions(-) delete mode 100644 framework/base/ViewContent.php diff --git a/framework/base/View.php b/framework/base/View.php index a794e08..b791743 100644 --- a/framework/base/View.php +++ b/framework/base/View.php @@ -10,6 +10,7 @@ namespace yii\base; use Yii; use yii\base\Application; use yii\helpers\FileHelper; +use yii\helpers\Html; /** * View represents a view object in the MVC pattern. @@ -22,22 +23,48 @@ use yii\helpers\FileHelper; class View extends Component { /** - * @event Event an event that is triggered by [[renderFile()]] right before it renders a view file. + * @event ViewEvent an event that is triggered by [[renderFile()]] right before it renders a view file. */ const EVENT_BEFORE_RENDER = 'beforeRender'; /** - * @event Event an event that is triggered by [[renderFile()]] right after it renders a view file. + * @event ViewEvent an event that is triggered by [[renderFile()]] right after it renders a view file. */ const EVENT_AFTER_RENDER = 'afterRender'; /** - * @var object the object that owns this view. This can be a controller, a widget, or any other object. + * The location of registered JavaScript code block or files. + * This means the location is in the head section. */ - public $context; + const POS_HEAD = 1; + /** + * The location of registered JavaScript code block or files. + * This means the location is at the beginning of the body section. + */ + const POS_BEGIN = 2; + /** + * The location of registered JavaScript code block or files. + * This means the location is at the end of the body section. + */ + const POS_END = 3; + /** + * This is internally used as the placeholder for receiving the content registered for the head section. + */ + const PL_HEAD = ''; + /** + * This is internally used as the placeholder for receiving the content registered for the beginning of the body section. + */ + const PL_BODY_BEGIN = ''; /** - * @var ViewContent + * This is internally used as the placeholder for receiving the content registered for the end of the body section. */ - public $page; + const PL_BODY_END = ''; + + + /** + * @var object the context under which the [[renderFile()]] method is being invoked. + * This can be a controller, a widget, or any other object. + */ + public $context; /** * @var mixed custom parameters that are shared among view templates. */ @@ -48,32 +75,75 @@ class View extends Component */ public $renderer; /** - * @var Theme|array the theme object or the configuration array for creating the theme. + * @var Theme|array the theme object or the configuration array for creating the theme object. * If not set, it means theming is not enabled. */ public $theme; /** * @var array a list of named output blocks. The keys are the block names and the values * are the corresponding block content. You can call [[beginBlock()]] and [[endBlock()]] - * to capture small fragments of a view. They can be later accessed at somewhere else + * to capture small fragments of a view. They can be later accessed somewhere else * through this property. */ public $blocks; /** * @var Widget[] the widgets that are currently being rendered (not ended). This property - * is maintained by [[beginWidget()]] and [[endWidget()]] methods. Do not modify it. + * is maintained by [[beginWidget()]] and [[endWidget()]] methods. Do not modify it directly. + * @internal */ public $widgetStack = array(); /** * @var array a list of currently active fragment cache widgets. This property - * is used internally to implement the content caching feature. Do not modify it. + * is used internally to implement the content caching feature. Do not modify it directly. + * @internal */ public $cacheStack = array(); /** * @var array a list of placeholders for embedding dynamic contents. This property - * is used internally to implement the content caching feature. Do not modify it. + * is used internally to implement the content caching feature. Do not modify it directly. + * @internal */ public $dynamicPlaceholders = array(); + /** + * @var array the registered asset bundles. The keys are the bundle names, and the values + * are the corresponding [[AssetBundle]] objects. + * @see registerAssetBundle + */ + public $assetBundles; + /** + * @var string the page title + */ + public $title; + /** + * @var array the registered meta tags. + * @see registerMetaTag + */ + public $metaTags; + /** + * @var array the registered link tags. + * @see registerLinkTag + */ + public $linkTags; + /** + * @var array the registered CSS code blocks. + * @see registerCss + */ + public $css; + /** + * @var array the registered CSS files. + * @see registerCssFile + */ + public $cssFiles; + /** + * @var array the registered JS code blocks + * @see registerJs + */ + public $js; + /** + * @var array the registered JS files. + * @see registerJsFile + */ + public $jsFiles; /** @@ -88,11 +158,6 @@ class View extends Component if (is_array($this->theme)) { $this->theme = Yii::createObject($this->theme); } - if (is_array($this->page)) { - $this->page = Yii::createObject($this->page); - } else { - $this->page = new ViewContent; - } } /** @@ -445,4 +510,273 @@ class View extends Component { $this->endWidget(); } + + + private $_assetManager; + + /** + * Registers the asset manager being used by this view object. + * @return \yii\web\AssetManager the asset manager. Defaults to the "assetManager" application component. + */ + public function getAssetManager() + { + return $this->_assetManager ?: Yii::$app->getAssetManager(); + } + + /** + * Sets the asset manager. + * @param \yii\web\AssetManager $value the asset manager + */ + public function setAssetManager($value) + { + $this->_assetManager = $value; + } + + /** + * Marks the beginning of an HTML page. + */ + public function beginPage() + { + ob_start(); + ob_implicit_flush(false); + } + + /** + * Marks the ending of an HTML page. + */ + public function endPage() + { + $content = ob_get_clean(); + echo strtr($content, array( + self::PL_HEAD => $this->renderHeadHtml(), + self::PL_BODY_BEGIN => $this->renderBodyBeginHtml(), + self::PL_BODY_END => $this->renderBodyEndHtml(), + )); + + unset( + $this->assetBundles, + $this->metaTags, + $this->linkTags, + $this->css, + $this->cssFiles, + $this->js, + $this->jsFiles + ); + } + + /** + * Marks the beginning of an HTML body section. + */ + public function beginBody() + { + echo self::PL_BODY_BEGIN; + } + + /** + * Marks the ending of an HTML body section. + */ + public function endBody() + { + echo self::PL_BODY_END; + } + + /** + * Marks the position of an HTML head section. + */ + public function head() + { + echo self::PL_HEAD; + } + + /** + * Registers the named asset bundle. + * All dependent asset bundles will be registered. + * @param string $name the name of the asset bundle. + * @throws InvalidConfigException if the asset bundle does not exist or a cyclic dependency is detected + */ + public function registerAssetBundle($name) + { + if (!isset($this->assetBundles[$name])) { + $am = $this->getAssetManager(); + $bundle = $am->getBundle($name); + if ($bundle !== null) { + $this->assetBundles[$name] = false; + $bundle->registerAssets($this); + $this->assetBundles[$name] = true; + } else { + throw new InvalidConfigException("Unknown asset bundle: $name"); + } + } elseif ($this->assetBundles[$name] === false) { + throw new InvalidConfigException("A cyclic dependency is detected for bundle '$name'."); + } + } + + /** + * Registers a meta tag. + * @param array $options the HTML attributes for the meta tag. + * @param string $key the key that identifies the meta tag. If two meta tags are registered + * with the same key, the latter will overwrite the former. If this is null, the new meta tag + * will be appended to the existing ones. + */ + public function registerMetaTag($options, $key = null) + { + if ($key === null) { + $this->metaTags[] = Html::tag('meta', '', $options); + } else { + $this->metaTags[$key] = Html::tag('meta', '', $options); + } + } + + /** + * Registers a link tag. + * @param array $options the HTML attributes for the link tag. + * @param string $key the key that identifies the link tag. If two link tags are registered + * with the same key, the latter will overwrite the former. If this is null, the new link tag + * will be appended to the existing ones. + */ + public function registerLinkTag($options, $key = null) + { + if ($key === null) { + $this->linkTags[] = Html::tag('link', '', $options); + } else { + $this->linkTags[$key] = Html::tag('link', '', $options); + } + } + + /** + * Registers a CSS code block. + * @param string $css the CSS code block to be registered + * @param array $options the HTML attributes for the style tag. + * @param string $key the key that identifies the CSS code block. If null, it will use + * $css as the key. If two CSS code blocks are registered with the same key, the latter + * will overwrite the former. + */ + public function registerCss($css, $options = array(), $key = null) + { + $key = $key ?: $css; + $this->css[$key] = Html::style($css, $options); + } + + /** + * Registers a CSS file. + * @param string $url the CSS file to be registered. + * @param array $options the HTML attributes for the link tag. + * @param string $key the key that identifies the CSS script file. If null, it will use + * $url as the key. If two CSS files are registered with the same key, the latter + * will overwrite the former. + */ + public function registerCssFile($url, $options = array(), $key = null) + { + $key = $key ?: $url; + $this->cssFiles[$key] = Html::cssFile($url, $options); + } + + /** + * Registers a JS code block. + * @param string $js the JS code block to be registered + * @param array $options the HTML attributes for the script tag. A special option + * named "position" is supported which specifies where the JS script tag should be inserted + * in a page. The possible values of "position" are: + * + * - [[POS_HEAD]]: in the head section + * - [[POS_BEGIN]]: at the beginning of the body section + * - [[POS_END]]: at the end of the body section + * + * @param string $key the key that identifies the JS code block. If null, it will use + * $js as the key. If two JS code blocks are registered with the same key, the latter + * will overwrite the former. + */ + public function registerJs($js, $options = array(), $key = null) + { + $position = isset($options['position']) ? $options['position'] : self::POS_END; + unset($options['position']); + $key = $key ?: $js; + $this->js[$position][$key] = Html::script($js, $options); + } + + /** + * Registers a JS file. + * @param string $url the JS file to be registered. + * @param array $options the HTML attributes for the script tag. A special option + * named "position" is supported which specifies where the JS script tag should be inserted + * in a page. The possible values of "position" are: + * + * - [[POS_HEAD]]: in the head section + * - [[POS_BEGIN]]: at the beginning of the body section + * - [[POS_END]]: at the end of the body section + * + * @param string $key the key that identifies the JS script file. If null, it will use + * $url as the key. If two JS files are registered with the same key, the latter + * will overwrite the former. + */ + public function registerJsFile($url, $options = array(), $key = null) + { + $position = isset($options['position']) ? $options['position'] : self::POS_END; + unset($options['position']); + $key = $key ?: $url; + $this->jsFiles[$position][$key] = Html::jsFile($url, $options); + } + + /** + * Renders the content to be inserted in the head section. + * The content is rendered using the registered meta tags, link tags, CSS/JS code blocks and files. + * @return string the rendered content + */ + protected function renderHeadHtml() + { + $lines = array(); + if (!empty($this->metaTags)) { + $lines[] = implode("\n", $this->cssFiles); + } + if (!empty($this->linkTags)) { + $lines[] = implode("\n", $this->cssFiles); + } + if (!empty($this->cssFiles)) { + $lines[] = implode("\n", $this->cssFiles); + } + if (!empty($this->css)) { + $lines[] = implode("\n", $this->css); + } + if (!empty($this->jsFiles[self::POS_HEAD])) { + $lines[] = implode("\n", $this->jsFiles[self::POS_HEAD]); + } + if (!empty($this->js[self::POS_HEAD])) { + $lines[] = implode("\n", $this->js[self::POS_HEAD]); + } + return implode("\n", $lines); + } + + /** + * Renders the content to be inserted at the beginning of the body section. + * The content is rendered using the registered JS code blocks and files. + * @return string the rendered content + */ + protected function renderBodyBeginHtml() + { + $lines = array(); + if (!empty($this->jsFiles[self::POS_BEGIN])) { + $lines[] = implode("\n", $this->jsFiles[self::POS_BEGIN]); + } + if (!empty($this->js[self::POS_BEGIN])) { + $lines[] = implode("\n", $this->js[self::POS_BEGIN]); + } + return implode("\n", $lines); + } + + /** + * Renders the content to be inserted at the end of the body section. + * The content is rendered using the registered JS code blocks and files. + * @return string the rendered content + */ + protected function renderBodyEndHtml() + { + $lines = array(); + if (!empty($this->jsFiles[self::POS_END])) { + $lines[] = implode("\n", $this->jsFiles[self::POS_END]); + } + if (!empty($this->js[self::POS_END])) { + $lines[] = implode("\n", $this->js[self::POS_END]); + } + return implode("\n", $lines); + } } \ No newline at end of file diff --git a/framework/base/ViewContent.php b/framework/base/ViewContent.php deleted file mode 100644 index c7a3bc4..0000000 --- a/framework/base/ViewContent.php +++ /dev/null @@ -1,235 +0,0 @@ - - * @since 2.0 - */ -class ViewContent extends Component -{ - const POS_HEAD = 1; - const POS_BEGIN = 2; - const POS_END = 3; - - const TOKEN_HEAD = ''; - const TOKEN_BODY_BEGIN = ''; - const TOKEN_BODY_END = ''; - - public $assetBundles; - public $title; - public $metaTags; - public $linkTags; - public $css; - public $cssFiles; - public $js; - public $jsFiles; - public $jsInHead; - public $jsFilesInHead; - public $jsInBody; - public $jsFilesInBody; - - public function reset() - { - $this->assetBundles = null; - $this->title = null; - $this->metaTags = null; - $this->linkTags = null; - $this->css = null; - $this->cssFiles = null; - $this->js = null; - $this->jsFiles = null; - $this->jsInHead = null; - $this->jsFilesInHead = null; - $this->jsInBody = null; - $this->jsFilesInBody = null; - } - - private $_assetManager; - - /** - * @return \yii\web\AssetManager - */ - public function getAssetManager() - { - return $this->_assetManager ?: Yii::$app->getAssets(); - } - - public function setAssetManager($value) - { - $this->_assetManager = $value; - } - - public function begin() - { - ob_start(); - ob_implicit_flush(false); - } - - public function end() - { - $content = ob_get_clean(); - echo $this->populate($content); - } - - public function beginBody() - { - echo self::TOKEN_BODY_BEGIN; - } - - public function endBody() - { - echo self::TOKEN_BODY_END; - } - - public function head() - { - echo self::TOKEN_HEAD; - } - - public function registerAssetBundle($name) - { - if (!isset($this->assetBundles[$name])) { - $am = $this->getAssetManager(); - $bundle = $am->getBundle($name); - if ($bundle !== null) { - $this->assetBundles[$name] = false; - $bundle->registerAssets($this, $am); - $this->assetBundles[$name] = true; - } else { - throw new InvalidConfigException("Unknown asset bundle: $name"); - } - } elseif ($this->assetBundles[$name] === false) { - throw new InvalidConfigException("A cyclic dependency is detected for bundle '$name'."); - } - } - - public function registerMetaTag($options, $key = null) - { - if ($key === null) { - $this->metaTags[] = Html::tag('meta', '', $options); - } else { - $this->metaTags[$key] = Html::tag('meta', '', $options); - } - } - - public function registerLinkTag($options, $key = null) - { - if ($key === null) { - $this->linkTags[] = Html::tag('link', '', $options); - } else { - $this->linkTags[$key] = Html::tag('link', '', $options); - } - } - - public function registerCss($css, $options = array(), $key = null) - { - $key = $key ?: $css; - $this->css[$key] = Html::style($css, $options); - } - - public function registerCssFile($url, $options = array(), $key = null) - { - $key = $key ?: $url; - $this->cssFiles[$key] = Html::cssFile($url, $options); - } - - public function registerJs($js, $options = array(), $key = null) - { - $position = isset($options['position']) ? $options['position'] : self::POS_END; - unset($options['position']); - $key = $key ?: $js; - $html = Html::script($js, $options); - if ($position == self::POS_END) { - $this->js[$key] = $html; - } elseif ($position == self::POS_HEAD) { - $this->jsInHead[$key] = $html; - } elseif ($position == self::POS_BEGIN) { - $this->jsInBody[$key] = $html; - } else { - throw new InvalidParamException("Unknown position: $position"); - } - } - - public function registerJsFile($url, $options = array(), $key = null) - { - $position = isset($options['position']) ? $options['position'] : self::POS_END; - unset($options['position']); - $key = $key ?: $url; - $html = Html::jsFile($url, $options); - if ($position == self::POS_END) { - $this->jsFiles[$key] = $html; - } elseif ($position == self::POS_HEAD) { - $this->jsFilesInHead[$key] = $html; - } elseif ($position == self::POS_BEGIN) { - $this->jsFilesInBody[$key] = $html; - } else { - throw new InvalidParamException("Unknown position: $position"); - } - } - - protected function populate($content) - { - return strtr($content, array( - self::TOKEN_HEAD => $this->getHeadHtml(), - self::TOKEN_BODY_BEGIN => $this->getBodyBeginHtml(), - self::TOKEN_BODY_END => $this->getBodyEndHtml(), - )); - } - - protected function getHeadHtml() - { - $lines = array(); - if (!empty($this->metaTags)) { - $lines[] = implode("\n", $this->cssFiles); - } - if (!empty($this->linkTags)) { - $lines[] = implode("\n", $this->cssFiles); - } - if (!empty($this->cssFiles)) { - $lines[] = implode("\n", $this->cssFiles); - } - if (!empty($this->css)) { - $lines[] = implode("\n", $this->css); - } - if (!empty($this->jsFilesInHead)) { - $lines[] = implode("\n", $this->jsFilesInHead); - } - if (!empty($this->jsInHead)) { - $lines[] = implode("\n", $this->jsInHead); - } - return implode("\n", $lines); - } - - protected function getBodyBeginHtml() - { - $lines = array(); - if (!empty($this->jsFilesInBody)) { - $lines[] = implode("\n", $this->jsFilesInBody); - } - if (!empty($this->jsInHead)) { - $lines[] = implode("\n", $this->jsInBody); - } - return implode("\n", $lines); - } - - protected function getBodyEndHtml() - { - $lines = array(); - if (!empty($this->jsFiles)) { - $lines[] = implode("\n", $this->jsFiles); - } - if (!empty($this->js)) { - $lines[] = implode("\n", $this->js); - } - return implode("\n", $lines); - } -} \ No newline at end of file diff --git a/framework/web/Application.php b/framework/web/Application.php index 90f7292..3387044 100644 --- a/framework/web/Application.php +++ b/framework/web/Application.php @@ -101,9 +101,9 @@ class Application extends \yii\base\Application * Returns the asset manager. * @return AssetManager the asset manager component */ - public function getAssets() + public function getAssetManager() { - return $this->getComponent('assets'); + return $this->getComponent('assetManager'); } /** @@ -126,7 +126,7 @@ class Application extends \yii\base\Application 'user' => array( 'class' => 'yii\web\User', ), - 'assets' => array( + 'assetManager' => array( 'class' => 'yii\web\AssetManager', ), )); diff --git a/framework/web/AssetBundle.php b/framework/web/AssetBundle.php index f82173b..4108b07 100644 --- a/framework/web/AssetBundle.php +++ b/framework/web/AssetBundle.php @@ -12,6 +12,14 @@ use yii\base\InvalidConfigException; use yii\base\Object; /** + * AssetBundle represents a collection of asset files, such as CSS, JS, images. + * + * Each asset bundle has a unique name that globally identifies it among all asset bundles + * used in an application. + * + * An asset bundle can depend on other asset bundles. When registering an asset bundle + * with a view, all its dependent asset bundles will be automatically registered. + * * @author Qiang Xue * @since 2.0 */ @@ -66,7 +74,7 @@ class AssetBundle extends Object * * Each JavaScript file may be associated with options. In this case, the array key * should be the JavaScript file path, while the corresponding array value should - * be the option array. The options will be passed to [[ViewContent::registerJsFile()]]. + * be the option array. The options will be passed to [[View::registerJsFile()]]. */ public $js = array(); /** @@ -78,7 +86,7 @@ class AssetBundle extends Object * * Each CSS file may be associated with options. In this case, the array key * should be the CSS file path, while the corresponding array value should - * be the option array. The options will be passed to [[ViewContent::registerCssFile()]]. + * be the option array. The options will be passed to [[View::registerCssFile()]]. */ public $css = array(); /** @@ -108,14 +116,20 @@ class AssetBundle extends Object } /** - * @param \yii\base\ViewContent $page - * @param AssetManager $am - * @throws InvalidConfigException + * Registers the CSS and JS files with the given view. + * This method will first register all dependent asset bundles. + * It will then try to convert non-CSS or JS files (e.g. LESS, Sass) into the corresponding + * CSS or JS files using [[AssetManager::converter|asset converter]]. + * @param \yii\base\View $view the view that the asset files to be registered with. + * @throws InvalidConfigException if [[baseUrl]] or [[basePath]] is not set when the bundle + * contains internal CSS or JS files. */ - public function registerAssets($page, $am) + public function registerAssets($view) { + $am = $view->getAssetManager(); + foreach ($this->depends as $name) { - $page->registerAssetBundle($name); + $view->registerAssetBundle($name); } if ($this->sourcePath !== null) { @@ -128,23 +142,23 @@ class AssetBundle extends Object $js = is_string($options) ? $options : $js; if (strpos($js, '/') !== 0 && strpos($js, '://') === false) { if (isset($this->basePath, $this->baseUrl)) { - $js = $converter->convert(ltrim($js, '/'), $this->basePath, $this->baseUrl); + $js = $converter->convert($js, $this->basePath, $this->baseUrl); } else { throw new InvalidConfigException('Both of the "baseUrl" and "basePath" properties must be set.'); } } - $page->registerJsFile($js, is_array($options) ? $options : array()); + $view->registerJsFile($js, is_array($options) ? $options : array()); } foreach ($this->css as $css => $options) { $css = is_string($options) ? $options : $css; - if (strpos($css, '//') !== 0 && strpos($css, '://') === false) { + if (strpos($css, '/') !== 0 && strpos($css, '://') === false) { if (isset($this->basePath, $this->baseUrl)) { - $css = $converter->convert(ltrim($css, '/'), $this->basePath, $this->baseUrl); + $css = $converter->convert($css, $this->basePath, $this->baseUrl); } else { throw new InvalidConfigException('Both of the "baseUrl" and "basePath" properties must be set.'); } } - $page->registerCssFile($css, is_array($options) ? $options : array()); + $view->registerCssFile($css, is_array($options) ? $options : array()); } } } \ No newline at end of file diff --git a/framework/web/AssetManager.php b/framework/web/AssetManager.php index c4ad385..95dcbd2 100644 --- a/framework/web/AssetManager.php +++ b/framework/web/AssetManager.php @@ -14,6 +14,7 @@ use yii\base\InvalidParamException; use yii\helpers\FileHelper; /** + * AssetManager manages asset bundles and asset publishing. * * @author Qiang Xue * @since 2.0 @@ -135,7 +136,8 @@ class AssetManager extends Component private $_converter; /** - * @return IAssetConverter + * Returns the asset converter. + * @return IAssetConverter the asset converter. */ public function getConverter() { @@ -149,6 +151,12 @@ class AssetManager extends Component return $this->_converter; } + /** + * Sets the asset converter. + * @param array|IAssetConverter $value the asset converter. This can be either + * an object implementing the [[IAssetConverter]] interface, or a configuration + * array that can be used to create the asset converter object. + */ public function setConverter($value) { $this->_converter = $value; From c7cf1026c0dfbea2eda74c24633f7777220729c9 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Sat, 20 Apr 2013 01:40:37 +0400 Subject: [PATCH 091/104] adjusted default app layout --- framework/console/webapp/default/protected/views/layouts/main.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/framework/console/webapp/default/protected/views/layouts/main.php b/framework/console/webapp/default/protected/views/layouts/main.php index a4215dc..5c883e6 100644 --- a/framework/console/webapp/default/protected/views/layouts/main.php +++ b/framework/console/webapp/default/protected/views/layouts/main.php @@ -3,10 +3,10 @@ - <?php echo Html::encode($this->page->title)?> + <?php echo Html::encode($this->title)?> -

    page->title)?>

    +

    title)?>

    From 304122ebe4e4ffaba958ec6b27012724cb3b1232 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Fri, 19 Apr 2013 20:44:59 -0400 Subject: [PATCH 092/104] Fixed bug in yiic.php. Refactoring AssetConverter. --- framework/console/Application.php | 1 + framework/console/controllers/ScriptController.php | 26 +++++++++++++++++++++ framework/web/AssetConverter.php | 27 ++++++++++++++++++---- framework/web/IAssetConverter.php | 10 ++++++++ framework/yiic.php | 7 +++--- 5 files changed, 62 insertions(+), 9 deletions(-) create mode 100644 framework/console/controllers/ScriptController.php diff --git a/framework/console/Application.php b/framework/console/Application.php index 574495b..e185cc5 100644 --- a/framework/console/Application.php +++ b/framework/console/Application.php @@ -129,6 +129,7 @@ class Application extends \yii\base\Application 'migrate' => 'yii\console\controllers\MigrateController', 'app' => 'yii\console\controllers\AppController', 'cache' => 'yii\console\controllers\CacheController', + 'script' => 'yii\console\controllers\ScriptController', ); } diff --git a/framework/console/controllers/ScriptController.php b/framework/console/controllers/ScriptController.php new file mode 100644 index 0000000..7ca498b --- /dev/null +++ b/framework/console/controllers/ScriptController.php @@ -0,0 +1,26 @@ + + * @since 2.0 + */ +class ScriptController extends Controller +{ + public $defaultAction = 'combo'; + + public function actionCombo($configFile) + { + + } +} \ No newline at end of file diff --git a/framework/web/AssetConverter.php b/framework/web/AssetConverter.php index 26cc59b..8340be5 100644 --- a/framework/web/AssetConverter.php +++ b/framework/web/AssetConverter.php @@ -11,18 +11,32 @@ use Yii; use yii\base\Component; /** + * AssetConverter supports conversion of several popular script formats into JS or CSS scripts. + * * @author Qiang Xue * @since 2.0 */ class AssetConverter extends Component implements IAssetConverter { + /** + * @var array the commands that are used to perform the asset conversion. + * The keys are the asset file extension names, and the values are the corresponding + * target script types (either "css" or "js") and the commands used for the conversion. + */ public $commands = array( - 'less' => array('css', 'lessc %s %s'), - 'scss' => array('css', 'sass %s %s'), - 'sass' => array('css', 'sass %s %s'), - 'styl' => array('js', 'stylus < %s > %s'), + 'less' => array('css', 'lessc {from} {to}'), + 'scss' => array('css', 'sass {from} {to}'), + 'sass' => array('css', 'sass {from} {to}'), + 'styl' => array('js', 'stylus < {from} > {to}'), ); + /** + * Converts a given asset file into a CSS or JS file. + * @param string $asset the asset file path, relative to $basePath + * @param string $basePath the directory the $asset is relative to. + * @param string $baseUrl the URL corresponding to $basePath + * @return string the URL to the converted asset file. + */ public function convert($asset, $basePath, $baseUrl) { $pos = strrpos($asset, '.'); @@ -33,7 +47,10 @@ class AssetConverter extends Component implements IAssetConverter $result = substr($asset, 0, $pos + 1) . $ext; if (@filemtime("$basePath/$result") < filemtime("$basePath/$asset")) { $output = array(); - $command = sprintf($command, "$basePath/$asset", "$basePath/$result"); + $command = strtr($command, array( + '{from}' => "$basePath/$asset", + '{to}' => "$basePath/$result", + )); exec($command, $output); Yii::info("Converted $asset into $result: " . implode("\n", $output), __METHOD__); return "$baseUrl/$result"; diff --git a/framework/web/IAssetConverter.php b/framework/web/IAssetConverter.php index 994cb2f..4334d3e 100644 --- a/framework/web/IAssetConverter.php +++ b/framework/web/IAssetConverter.php @@ -8,10 +8,20 @@ namespace yii\web; /** + * The IAssetConverter interface must be implemented by asset converter classes. + * * @author Qiang Xue * @since 2.0 */ interface IAssetConverter { + /** + * Converts a given asset file into a CSS or JS file. + * @param string $asset the asset file path, relative to $basePath + * @param string $basePath the directory the $asset is relative to. + * @param string $baseUrl the URL corresponding to $basePath + * @return string the URL to the converted asset file. If the given asset does not + * need conversion, "$baseUrl/$asset" should be returned. + */ public function convert($asset, $basePath, $baseUrl); } \ No newline at end of file diff --git a/framework/yiic.php b/framework/yiic.php index 0db69bb..3872e2f 100644 --- a/framework/yiic.php +++ b/framework/yiic.php @@ -14,10 +14,9 @@ defined('STDIN') or define('STDIN', fopen('php://stdin', 'r')); require(__DIR__ . '/yii.php'); -$id = 'yiic'; -$basePath = __DIR__ . '/console'; - -$application = new yii\console\Application($id, $basePath, array( +$application = new yii\console\Application(array( + 'id' => 'yiic', + 'basePath' => __DIR__ . '/console', 'controllerPath' => '@yii/console/controllers', )); $application->run(); From 7775e927e12e2ba6a81d7046df0e228df2b1b608 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Mon, 22 Apr 2013 06:50:22 -0400 Subject: [PATCH 093/104] script command WIP --- framework/console/controllers/ScriptController.php | 156 ++++++++++++++++++++- framework/web/AssetBundle.php | 15 +- 2 files changed, 165 insertions(+), 6 deletions(-) diff --git a/framework/console/controllers/ScriptController.php b/framework/console/controllers/ScriptController.php index 7ca498b..44a5818 100644 --- a/framework/console/controllers/ScriptController.php +++ b/framework/console/controllers/ScriptController.php @@ -17,10 +17,160 @@ use yii\console\Controller; */ class ScriptController extends Controller { - public $defaultAction = 'combo'; + public $defaultAction = 'compress'; - public function actionCombo($configFile) + public $bundles = array(); + public $extensions = array(); + /** + * @var array + * ~~~ + * 'all' => array( + * 'css' => 'all.css', + * 'js' => 'js.css', + * 'depends' => array( ... ), + * ) + * ~~~ + */ + public $targets = array(); + public $basePath; + public $baseUrl; + public $publishOptions = array(); + + public function actionCompress($configFile, $bundleFile) + { + $this->loadConfiguration($configFile); + $bundles = $this->loadBundles($this->bundles, $this->extensions); + $this->publishBundles($bundles, $this->publishOptions); + $timestamp = time(); + $targets = array(); + foreach ($this->targets as $name => $target) { + $target['basePath'] = $this->basePath; + $target['baseUrl'] = $this->baseUrl; + if (isset($target['js'])) { + $this->buildTarget($target, 'js', $bundles, $timestamp); + } + if (isset($target['css'])) { + $this->buildTarget($target, 'css', $bundles, $timestamp); + } + $targets[$name] = $target; + } + + $targets = $this->adjustDependency($targets, $bundles); + $array = var_export($targets, true); + $version = date('Y-m-d H:i:s', time()); + file_put_contents($bundleFile, << $value) { + if (property_exists($this, $name)) { + $this->$name = $value; + } else { + throw new Exception("Unknown configuration: $name"); + } + } + + if (!isset($this->basePath)) { + throw new Exception("Please specify the 'basePath' option."); + } + if (!is_dir($this->basePath)) { + throw new Exception("The 'basePath' directory does not exist: {$this->basePath}"); + } + if (!isset($this->baseUrl)) { + throw new Exception("Please specify the 'baseUrl' option."); + } + $this->publishOptions['basePath'] = $this->basePath; + $this->publishOptions['baseUrl'] = $this->baseUrl; + } + + protected function loadBundles($bundles, $extensions) + { + $result = array(); + foreach ($bundles as $name => $bundle) { + $bundle['class'] = 'yii\\web\\AssetBundle'; + $result[$name] = Yii::createObject($bundle); + } + foreach ($extensions as $path) { + $manifest = $path . '/assets.php'; + if (!is_file($manifest)) { + continue; + } + foreach (require($manifest) as $name => $bundle) { + if (!isset($result[$name])) { + $bundle['class'] = 'yii\\web\\AssetBundle'; + $result[$name] = Yii::createObject($bundle); + } + } + } + return $result; + } + + /** + * @param \yii\web\AssetBundle[] $bundles + * @param array $options + */ + protected function publishBundles($bundles, $options) { - + if (!isset($options['class'])) { + $options['class'] = 'yii\\web\\AssetManager'; + } + $am = Yii::createObject($options); + foreach ($bundles as $bundle) { + $bundle->publish($am); + } + } + + /** + * @param array $target + * @param string $type either "js" or "css" + * @param \yii\web\AssetBundle[] $bundles + * @param integer $timestamp + * @throws Exception + */ + protected function buildTarget(&$target, $type, $bundles, $timestamp) + { + $outputFile = strtr($target[$type], array( + '{ts}' => $timestamp, + )); + $inputFiles = array(); + foreach ($target['depends'] as $name) { + if (isset($bundles[$name])) { + foreach ($bundles[$name]->$type as $file) { + $inputFiles[] = $bundles[$name]->basePath . '/' . $file; + } + } else { + throw new Exception("Unknown bundle: $name"); + } + } + if ($type === 'js') { + $this->compressJsFiles($inputFiles, $target['basePath'] . '/' . $outputFile); + } else { + $this->compressCssFiles($inputFiles, $target['basePath'] . '/' . $outputFile); + } + $target[$type] = array($outputFile); + } + + protected function adjustDependency($targets, $bundles) + { + return $targets; + } + + protected function compressJsFiles($inputFiles, $outputFile) + { + + } + + protected function compressCssFiles($inputFiles, $outputFile) + { + } } \ No newline at end of file diff --git a/framework/web/AssetBundle.php b/framework/web/AssetBundle.php index 4108b07..9fc2bbf 100644 --- a/framework/web/AssetBundle.php +++ b/framework/web/AssetBundle.php @@ -132,9 +132,7 @@ class AssetBundle extends Object $view->registerAssetBundle($name); } - if ($this->sourcePath !== null) { - list ($this->basePath, $this->baseUrl) = $am->publish($this->sourcePath, $this->publishOptions); - } + $this->publish($am); $converter = $am->getConverter(); @@ -161,4 +159,15 @@ class AssetBundle extends Object $view->registerCssFile($css, is_array($options) ? $options : array()); } } + + /** + * Publishes the asset bundle if its source code is not under Web-accessible directory. + * @param AssetManager $am the asset manager to perform the asset publishing + */ + public function publish($am) + { + if ($this->sourcePath !== null) { + list ($this->basePath, $this->baseUrl) = $am->publish($this->sourcePath, $this->publishOptions); + } + } } \ No newline at end of file From f21499dd9b10de70438e11ad1099de87337b13bf Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Mon, 22 Apr 2013 16:03:27 -0400 Subject: [PATCH 094/104] script WIP --- framework/base/View.php | 4 +- framework/console/controllers/ScriptController.php | 158 ++++++++++++++++----- framework/web/AssetBundle.php | 69 ++++----- 3 files changed, 157 insertions(+), 74 deletions(-) diff --git a/framework/base/View.php b/framework/base/View.php index b791743..10a7053 100644 --- a/framework/base/View.php +++ b/framework/base/View.php @@ -592,7 +592,7 @@ class View extends Component * Registers the named asset bundle. * All dependent asset bundles will be registered. * @param string $name the name of the asset bundle. - * @throws InvalidConfigException if the asset bundle does not exist or a cyclic dependency is detected + * @throws InvalidConfigException if the asset bundle does not exist or a circular dependency is detected */ public function registerAssetBundle($name) { @@ -607,7 +607,7 @@ class View extends Component throw new InvalidConfigException("Unknown asset bundle: $name"); } } elseif ($this->assetBundles[$name] === false) { - throw new InvalidConfigException("A cyclic dependency is detected for bundle '$name'."); + throw new InvalidConfigException("A circular dependency is detected for bundle '$name'."); } } diff --git a/framework/console/controllers/ScriptController.php b/framework/console/controllers/ScriptController.php index 44a5818..ab57f92 100644 --- a/framework/console/controllers/ScriptController.php +++ b/framework/console/controllers/ScriptController.php @@ -32,41 +32,26 @@ class ScriptController extends Controller * ~~~ */ public $targets = array(); - public $basePath; - public $baseUrl; - public $publishOptions = array(); + public $assetManager = array(); public function actionCompress($configFile, $bundleFile) { $this->loadConfiguration($configFile); $bundles = $this->loadBundles($this->bundles, $this->extensions); - $this->publishBundles($bundles, $this->publishOptions); + $targets = $this->loadTargets($this->targets, $bundles); +// $this->publishBundles($bundles, $this->publishOptions); $timestamp = time(); - $targets = array(); - foreach ($this->targets as $name => $target) { - $target['basePath'] = $this->basePath; - $target['baseUrl'] = $this->baseUrl; - if (isset($target['js'])) { + foreach ($targets as $target) { + if (!empty($target->js)) { $this->buildTarget($target, 'js', $bundles, $timestamp); } - if (isset($target['css'])) { + if (!empty($target->css)) { $this->buildTarget($target, 'css', $bundles, $timestamp); } - $targets[$name] = $target; } $targets = $this->adjustDependency($targets, $bundles); - $array = var_export($targets, true); - $version = date('Y-m-d H:i:s', time()); - file_put_contents($bundleFile, <<saveTargets($targets, $bundleFile); } protected function loadConfiguration($configFile) @@ -75,21 +60,16 @@ EOD if (property_exists($this, $name)) { $this->$name = $value; } else { - throw new Exception("Unknown configuration: $name"); + throw new Exception("Unknown configuration option: $name"); } } - if (!isset($this->basePath)) { - throw new Exception("Please specify the 'basePath' option."); - } - if (!is_dir($this->basePath)) { - throw new Exception("The 'basePath' directory does not exist: {$this->basePath}"); + if (!isset($this->assetManager['basePath'])) { + throw new Exception("Please specify 'basePath' for the 'assetManager' option."); } - if (!isset($this->baseUrl)) { - throw new Exception("Please specify the 'baseUrl' option."); + if (!isset($this->assetManager['baseUrl'])) { + throw new Exception("Please specify 'baseUrl' for the 'assetManager' option."); } - $this->publishOptions['basePath'] = $this->basePath; - $this->publishOptions['baseUrl'] = $this->baseUrl; } protected function loadBundles($bundles, $extensions) @@ -114,6 +94,33 @@ EOD return $result; } + protected function loadTargets($targets, $bundles) + { + $registered = array(); + foreach ($bundles as $name => $bundle) { + $this->registerBundle($bundles, $name, $registered); + } + $bundleOrders = array_combine(array_keys($registered), range(0, count($bundles) - 1)); + foreach ($targets as $name => $target) { + if (!isset($target['basePath'])) { + throw new Exception("Please specify 'basePath' for the '$name' target."); + } + if (!isset($target['baseUrl'])) { + throw new Exception("Please specify 'baseUrl' for the '$name' target."); + } + usort($target['depends'], function ($a, $b) use ($bundleOrders) { + if ($bundleOrders[$a] == $bundleOrders[$b]) { + return 0; + } else { + return $bundleOrders[$a] > $bundleOrders[$b] ? 1 : -1; + } + }); + $target['class'] = 'yii\\web\\AssetBundle'; + $targets[$name] = Yii::createObject($target); + } + return $targets; + } + /** * @param \yii\web\AssetBundle[] $bundles * @param array $options @@ -130,19 +137,20 @@ EOD } /** - * @param array $target + * @param \yii\web\AssetBundle $target * @param string $type either "js" or "css" * @param \yii\web\AssetBundle[] $bundles * @param integer $timestamp * @throws Exception */ - protected function buildTarget(&$target, $type, $bundles, $timestamp) + protected function buildTarget($target, $type, $bundles, $timestamp) { - $outputFile = strtr($target[$type], array( + $outputFile = strtr($target->$type, array( '{ts}' => $timestamp, )); $inputFiles = array(); - foreach ($target['depends'] as $name) { + + foreach ($target->depends as $name) { if (isset($bundles[$name])) { foreach ($bundles[$name]->$type as $file) { $inputFiles[] = $bundles[$name]->basePath . '/' . $file; @@ -152,18 +160,90 @@ EOD } } if ($type === 'js') { - $this->compressJsFiles($inputFiles, $target['basePath'] . '/' . $outputFile); + $this->compressJsFiles($inputFiles, $target->basePath . '/' . $outputFile); } else { - $this->compressCssFiles($inputFiles, $target['basePath'] . '/' . $outputFile); + $this->compressCssFiles($inputFiles, $target->basePath . '/' . $outputFile); } - $target[$type] = array($outputFile); + $target->$type = array($outputFile); } protected function adjustDependency($targets, $bundles) { + $map = array(); + foreach ($targets as $name => $target) { + foreach ($target->depends as $bundle) { + if (!isset($map[$bundle])) { + $map[$bundle] = $name; + } else { + throw new Exception("Bundle '$bundle' is found in both target '{$map[$bundle]}' and '$name'."); + } + } + } + + foreach ($targets as $name => $target) { + $depends = array(); + foreach ($target->depends as $bn) { + foreach ($bundles[$bn]->depends as $bundle) { + $depends[$map[$bundle]] = true; + } + } + unset($depends[$name]); + $target->depends = array_keys($depends); + } + + // detect possible circular dependencies + foreach ($targets as $name => $target) { + $registered = array(); + $this->registerBundle($targets, $name, $registered); + } + + foreach ($map as $bundle => $target) { + $targets[$bundle] = Yii::createObject(array( + 'class' => 'yii\\web\\AssetBundle', + 'depends' => array($target), + )); + } return $targets; } + protected function registerBundle($bundles, $name, &$registered) + { + if (!isset($registered[$name])) { + $registered[$name] = false; + $bundle = $bundles[$name]; + foreach ($bundle->depends as $depend) { + $this->registerBundle($bundles, $depend, $registered); + } + unset($registered[$name]); + $registered[$name] = true; + } elseif ($registered[$name] === false) { + throw new Exception("A circular dependency is detected for target '$name'."); + } + } + + protected function saveTargets($targets, $bundleFile) + { + $array = array(); + foreach ($targets as $name => $target) { + foreach (array('js', 'css', 'depends', 'basePath', 'baseUrl') as $prop) { + if (!empty($target->$prop)) { + $array[$name][$prop] = $target->$prop; + } + } + } + $array = var_export($array, true); + $version = date('Y-m-d H:i:s', time()); + file_put_contents($bundleFile, <<getAssetManager(); - foreach ($this->depends as $name) { $view->registerAssetBundle($name); } - $this->publish($am); + $this->publish($view->getAssetManager()); - $converter = $am->getConverter(); + foreach ($this->js as $js) { + $view->registerJsFile($js, $this->jsOptions); + } + foreach ($this->css as $css) { + $view->registerCssFile($css, $this->cssOptions); + } + } - foreach ($this->js as $js => $options) { - $js = is_string($options) ? $options : $js; + /** + * Publishes the asset bundle if its source code is not under Web-accessible directory. + * @param AssetManager $am the asset manager to perform the asset publishing + * @throws InvalidConfigException if [[baseUrl]] or [[basePath]] is not set when the bundle + * contains internal CSS or JS files. + */ + public function publish($am) + { + if ($this->sourcePath !== null) { + list ($this->basePath, $this->baseUrl) = $am->publish($this->sourcePath, $this->publishOptions); + } + $converter = $am->getConverter(); + foreach ($this->js as $i => $js) { if (strpos($js, '/') !== 0 && strpos($js, '://') === false) { if (isset($this->basePath, $this->baseUrl)) { - $js = $converter->convert($js, $this->basePath, $this->baseUrl); + $this->js[$i] = $converter->convert($js, $this->basePath, $this->baseUrl); } else { throw new InvalidConfigException('Both of the "baseUrl" and "basePath" properties must be set.'); } } - $view->registerJsFile($js, is_array($options) ? $options : array()); } - foreach ($this->css as $css => $options) { - $css = is_string($options) ? $options : $css; + foreach ($this->css as $i => $css) { if (strpos($css, '/') !== 0 && strpos($css, '://') === false) { if (isset($this->basePath, $this->baseUrl)) { - $css = $converter->convert($css, $this->basePath, $this->baseUrl); + $this->css[$i] = $converter->convert($css, $this->basePath, $this->baseUrl); } else { throw new InvalidConfigException('Both of the "baseUrl" and "basePath" properties must be set.'); } } - $view->registerCssFile($css, is_array($options) ? $options : array()); - } - } - - /** - * Publishes the asset bundle if its source code is not under Web-accessible directory. - * @param AssetManager $am the asset manager to perform the asset publishing - */ - public function publish($am) - { - if ($this->sourcePath !== null) { - list ($this->basePath, $this->baseUrl) = $am->publish($this->sourcePath, $this->publishOptions); } } } \ No newline at end of file From e132ed8d185a12edf4154a45af67ac34995ea9d0 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Mon, 22 Apr 2013 17:22:38 -0400 Subject: [PATCH 095/104] initial implementation of asset command. --- framework/console/Application.php | 2 +- framework/console/controllers/AssetController.php | 353 +++++++++++++++++++++ framework/console/controllers/ScriptController.php | 256 --------------- 3 files changed, 354 insertions(+), 257 deletions(-) create mode 100644 framework/console/controllers/AssetController.php delete mode 100644 framework/console/controllers/ScriptController.php diff --git a/framework/console/Application.php b/framework/console/Application.php index e185cc5..2f28cac 100644 --- a/framework/console/Application.php +++ b/framework/console/Application.php @@ -129,7 +129,7 @@ class Application extends \yii\base\Application 'migrate' => 'yii\console\controllers\MigrateController', 'app' => 'yii\console\controllers\AppController', 'cache' => 'yii\console\controllers\CacheController', - 'script' => 'yii\console\controllers\ScriptController', + 'asset' => 'yii\console\controllers\AssetController', ); } diff --git a/framework/console/controllers/AssetController.php b/framework/console/controllers/AssetController.php new file mode 100644 index 0000000..71a2cae --- /dev/null +++ b/framework/console/controllers/AssetController.php @@ -0,0 +1,353 @@ + + * @since 2.0 + */ +class AssetController extends Controller +{ + public $defaultAction = 'compress'; + + public $bundles = array(); + public $extensions = array(); + /** + * @var array + * ~~~ + * 'all' => array( + * 'css' => 'all.css', + * 'js' => 'js.css', + * 'depends' => array( ... ), + * ) + * ~~~ + */ + public $targets = array(); + public $assetManager = array(); + public $jsCompressor = 'java -jar compiler.jar --js {from} --js_output_file {to}'; + public $cssCompressor = 'java -jar yuicompressor.jar {from} -o {to}'; + + public function actionCompress($configFile, $bundleFile) + { + $this->loadConfiguration($configFile); + $bundles = $this->loadBundles($this->bundles, $this->extensions); + $targets = $this->loadTargets($this->targets, $bundles); + $this->publishBundles($bundles, $this->publishOptions); + $timestamp = time(); + foreach ($targets as $target) { + if (!empty($target->js)) { + $this->buildTarget($target, 'js', $bundles, $timestamp); + } + if (!empty($target->css)) { + $this->buildTarget($target, 'css', $bundles, $timestamp); + } + } + + $targets = $this->adjustDependency($targets, $bundles); + $this->saveTargets($targets, $bundleFile); + } + + protected function loadConfiguration($configFile) + { + foreach (require($configFile) as $name => $value) { + if (property_exists($this, $name)) { + $this->$name = $value; + } else { + throw new Exception("Unknown configuration option: $name"); + } + } + + if (!isset($this->assetManager['basePath'])) { + throw new Exception("Please specify 'basePath' for the 'assetManager' option."); + } + if (!isset($this->assetManager['baseUrl'])) { + throw new Exception("Please specify 'baseUrl' for the 'assetManager' option."); + } + } + + protected function loadBundles($bundles, $extensions) + { + $result = array(); + foreach ($bundles as $name => $bundle) { + $bundle['class'] = 'yii\\web\\AssetBundle'; + $result[$name] = Yii::createObject($bundle); + } + foreach ($extensions as $path) { + $manifest = $path . '/assets.php'; + if (!is_file($manifest)) { + continue; + } + foreach (require($manifest) as $name => $bundle) { + if (!isset($result[$name])) { + $bundle['class'] = 'yii\\web\\AssetBundle'; + $result[$name] = Yii::createObject($bundle); + } + } + } + return $result; + } + + protected function loadTargets($targets, $bundles) + { + // build the dependency order of bundles + $registered = array(); + foreach ($bundles as $name => $bundle) { + $this->registerBundle($bundles, $name, $registered); + } + $bundleOrders = array_combine(array_keys($registered), range(0, count($bundles) - 1)); + + // fill up the target which has empty 'depends'. + $referenced = array(); + foreach ($targets as $name => $target) { + if (empty($target['depends'])) { + if (!isset($all)) { + $all = $name; + } else { + throw new Exception("Only one target can have empty 'depends' option. Found two now: $all, $name"); + } + } else { + foreach ($target['depends'] as $bundle) { + if (!isset($referenced[$bundle])) { + $referenced[$bundle] = $name; + } else { + throw new Exception("Target '{$referenced[$bundle]}' and '$name' cannot contain the bundle '$bundle' at the same time."); + } + } + } + } + if (isset($all)) { + $targets[$all]['depends'] = array_diff(array_keys($registered), array_keys($referenced)); + } + + // adjust the 'depends' order for each target according to the dependency order of bundles + // create an AssetBundle object for each target + foreach ($targets as $name => $target) { + if (!isset($target['basePath'])) { + throw new Exception("Please specify 'basePath' for the '$name' target."); + } + if (!isset($target['baseUrl'])) { + throw new Exception("Please specify 'baseUrl' for the '$name' target."); + } + usort($target['depends'], function ($a, $b) use ($bundleOrders) { + if ($bundleOrders[$a] == $bundleOrders[$b]) { + return 0; + } else { + return $bundleOrders[$a] > $bundleOrders[$b] ? 1 : -1; + } + }); + $target['class'] = 'yii\\web\\AssetBundle'; + $targets[$name] = Yii::createObject($target); + } + return $targets; + } + + /** + * @param \yii\web\AssetBundle[] $bundles + * @param array $options + */ + protected function publishBundles($bundles, $options) + { + if (!isset($options['class'])) { + $options['class'] = 'yii\\web\\AssetManager'; + } + $am = Yii::createObject($options); + foreach ($bundles as $bundle) { + $bundle->publish($am); + } + } + + /** + * @param \yii\web\AssetBundle $target + * @param string $type either "js" or "css" + * @param \yii\web\AssetBundle[] $bundles + * @param integer $timestamp + * @throws Exception + */ + protected function buildTarget($target, $type, $bundles, $timestamp) + { + $outputFile = strtr($target->$type, array( + '{ts}' => $timestamp, + )); + $inputFiles = array(); + + foreach ($target->depends as $name) { + if (isset($bundles[$name])) { + foreach ($bundles[$name]->$type as $file) { + $inputFiles[] = $bundles[$name]->basePath . '/' . $file; + } + } else { + throw new Exception("Unknown bundle: $name"); + } + } + if ($type === 'js') { + $this->compressJsFiles($inputFiles, $target->basePath . '/' . $outputFile); + } else { + $this->compressCssFiles($inputFiles, $target->basePath . '/' . $outputFile); + } + $target->$type = array($outputFile); + } + + protected function adjustDependency($targets, $bundles) + { + $map = array(); + foreach ($targets as $name => $target) { + foreach ($target->depends as $bundle) { + $map[$bundle] = $name; + } + } + + foreach ($targets as $name => $target) { + $depends = array(); + foreach ($target->depends as $bn) { + foreach ($bundles[$bn]->depends as $bundle) { + $depends[$map[$bundle]] = true; + } + } + unset($depends[$name]); + $target->depends = array_keys($depends); + } + + // detect possible circular dependencies + foreach ($targets as $name => $target) { + $registered = array(); + $this->registerBundle($targets, $name, $registered); + } + + foreach ($map as $bundle => $target) { + $targets[$bundle] = Yii::createObject(array( + 'class' => 'yii\\web\\AssetBundle', + 'depends' => array($target), + )); + } + return $targets; + } + + protected function registerBundle($bundles, $name, &$registered) + { + if (!isset($registered[$name])) { + $registered[$name] = false; + $bundle = $bundles[$name]; + foreach ($bundle->depends as $depend) { + $this->registerBundle($bundles, $depend, $registered); + } + unset($registered[$name]); + $registered[$name] = true; + } elseif ($registered[$name] === false) { + throw new Exception("A circular dependency is detected for target '$name'."); + } + } + + protected function saveTargets($targets, $bundleFile) + { + $array = array(); + foreach ($targets as $name => $target) { + foreach (array('js', 'css', 'depends', 'basePath', 'baseUrl') as $prop) { + if (!empty($target->$prop)) { + $array[$name][$prop] = $target->$prop; + } + } + } + $array = var_export($array, true); + $version = date('Y-m-d H:i:s', time()); + file_put_contents($bundleFile, <<jsCompressor)) { + $tmpFile = $outputFile . '.tmp'; + $this->combineJsFiles($inputFiles, $tmpFile); + $log = shell_exec(strtr($this->jsCompressor, array( + '{from}' => $tmpFile, + '{to}' => $outputFile, + ))); + @unlink($tmpFile); + } else { + $log = call_user_func($this->jsCompressor, $this, $inputFiles, $outputFile); + } + } + + protected function compressCssFiles($inputFiles, $outputFile) + { + if (is_string($this->cssCompressor)) { + $tmpFile = $outputFile . '.tmp'; + $this->combineCssFiles($inputFiles, $tmpFile); + $log = shell_exec(strtr($this->cssCompressor, array( + '{from}' => $inputFiles, + '{to}' => $outputFile, + ))); + } else { + $log = call_user_func($this->cssCompressor, $this, $inputFiles, $outputFile); + } + } + + public function combineJsFiles($files, $tmpFile) + { + $content = ''; + foreach ($files as $file) { + $content .= "/*** BEGIN FILE: $file ***/\n" + . file_get_contents($file) + . "/*** END FILE: $file ***/\n"; + } + file_put_contents($tmpFile, $content); + } + + public function combineCssFiles($files, $tmpFile) + { + // todo: adjust url() references in CSS files + $content = ''; + foreach ($files as $file) { + $content .= "/*** BEGIN FILE: $file ***/\n" + . file_get_contents($file) + . "/*** END FILE: $file ***/\n"; + } + file_put_contents($tmpFile, $content); + } + + public function actionTemplate($configFile) + { + $template = << require('path/to/bundles.php'), + // + 'extensions' => require('path/to/namespaces.php'), + // + 'targets' => array( + 'all' => array( + 'basePath' => __DIR__, + 'baseUrl' => '/test', + 'js' => 'all-{ts}.js', + 'css' => 'all-{ts}.css', + ), + ), + + 'assetManager' => array( + 'basePath' => __DIR__, + 'baseUrl' => '/test', + ), +); +EOD; + file_put_contents($configFile, $template); + } +} \ No newline at end of file diff --git a/framework/console/controllers/ScriptController.php b/framework/console/controllers/ScriptController.php deleted file mode 100644 index ab57f92..0000000 --- a/framework/console/controllers/ScriptController.php +++ /dev/null @@ -1,256 +0,0 @@ - - * @since 2.0 - */ -class ScriptController extends Controller -{ - public $defaultAction = 'compress'; - - public $bundles = array(); - public $extensions = array(); - /** - * @var array - * ~~~ - * 'all' => array( - * 'css' => 'all.css', - * 'js' => 'js.css', - * 'depends' => array( ... ), - * ) - * ~~~ - */ - public $targets = array(); - public $assetManager = array(); - - public function actionCompress($configFile, $bundleFile) - { - $this->loadConfiguration($configFile); - $bundles = $this->loadBundles($this->bundles, $this->extensions); - $targets = $this->loadTargets($this->targets, $bundles); -// $this->publishBundles($bundles, $this->publishOptions); - $timestamp = time(); - foreach ($targets as $target) { - if (!empty($target->js)) { - $this->buildTarget($target, 'js', $bundles, $timestamp); - } - if (!empty($target->css)) { - $this->buildTarget($target, 'css', $bundles, $timestamp); - } - } - - $targets = $this->adjustDependency($targets, $bundles); - $this->saveTargets($targets, $bundleFile); - } - - protected function loadConfiguration($configFile) - { - foreach (require($configFile) as $name => $value) { - if (property_exists($this, $name)) { - $this->$name = $value; - } else { - throw new Exception("Unknown configuration option: $name"); - } - } - - if (!isset($this->assetManager['basePath'])) { - throw new Exception("Please specify 'basePath' for the 'assetManager' option."); - } - if (!isset($this->assetManager['baseUrl'])) { - throw new Exception("Please specify 'baseUrl' for the 'assetManager' option."); - } - } - - protected function loadBundles($bundles, $extensions) - { - $result = array(); - foreach ($bundles as $name => $bundle) { - $bundle['class'] = 'yii\\web\\AssetBundle'; - $result[$name] = Yii::createObject($bundle); - } - foreach ($extensions as $path) { - $manifest = $path . '/assets.php'; - if (!is_file($manifest)) { - continue; - } - foreach (require($manifest) as $name => $bundle) { - if (!isset($result[$name])) { - $bundle['class'] = 'yii\\web\\AssetBundle'; - $result[$name] = Yii::createObject($bundle); - } - } - } - return $result; - } - - protected function loadTargets($targets, $bundles) - { - $registered = array(); - foreach ($bundles as $name => $bundle) { - $this->registerBundle($bundles, $name, $registered); - } - $bundleOrders = array_combine(array_keys($registered), range(0, count($bundles) - 1)); - foreach ($targets as $name => $target) { - if (!isset($target['basePath'])) { - throw new Exception("Please specify 'basePath' for the '$name' target."); - } - if (!isset($target['baseUrl'])) { - throw new Exception("Please specify 'baseUrl' for the '$name' target."); - } - usort($target['depends'], function ($a, $b) use ($bundleOrders) { - if ($bundleOrders[$a] == $bundleOrders[$b]) { - return 0; - } else { - return $bundleOrders[$a] > $bundleOrders[$b] ? 1 : -1; - } - }); - $target['class'] = 'yii\\web\\AssetBundle'; - $targets[$name] = Yii::createObject($target); - } - return $targets; - } - - /** - * @param \yii\web\AssetBundle[] $bundles - * @param array $options - */ - protected function publishBundles($bundles, $options) - { - if (!isset($options['class'])) { - $options['class'] = 'yii\\web\\AssetManager'; - } - $am = Yii::createObject($options); - foreach ($bundles as $bundle) { - $bundle->publish($am); - } - } - - /** - * @param \yii\web\AssetBundle $target - * @param string $type either "js" or "css" - * @param \yii\web\AssetBundle[] $bundles - * @param integer $timestamp - * @throws Exception - */ - protected function buildTarget($target, $type, $bundles, $timestamp) - { - $outputFile = strtr($target->$type, array( - '{ts}' => $timestamp, - )); - $inputFiles = array(); - - foreach ($target->depends as $name) { - if (isset($bundles[$name])) { - foreach ($bundles[$name]->$type as $file) { - $inputFiles[] = $bundles[$name]->basePath . '/' . $file; - } - } else { - throw new Exception("Unknown bundle: $name"); - } - } - if ($type === 'js') { - $this->compressJsFiles($inputFiles, $target->basePath . '/' . $outputFile); - } else { - $this->compressCssFiles($inputFiles, $target->basePath . '/' . $outputFile); - } - $target->$type = array($outputFile); - } - - protected function adjustDependency($targets, $bundles) - { - $map = array(); - foreach ($targets as $name => $target) { - foreach ($target->depends as $bundle) { - if (!isset($map[$bundle])) { - $map[$bundle] = $name; - } else { - throw new Exception("Bundle '$bundle' is found in both target '{$map[$bundle]}' and '$name'."); - } - } - } - - foreach ($targets as $name => $target) { - $depends = array(); - foreach ($target->depends as $bn) { - foreach ($bundles[$bn]->depends as $bundle) { - $depends[$map[$bundle]] = true; - } - } - unset($depends[$name]); - $target->depends = array_keys($depends); - } - - // detect possible circular dependencies - foreach ($targets as $name => $target) { - $registered = array(); - $this->registerBundle($targets, $name, $registered); - } - - foreach ($map as $bundle => $target) { - $targets[$bundle] = Yii::createObject(array( - 'class' => 'yii\\web\\AssetBundle', - 'depends' => array($target), - )); - } - return $targets; - } - - protected function registerBundle($bundles, $name, &$registered) - { - if (!isset($registered[$name])) { - $registered[$name] = false; - $bundle = $bundles[$name]; - foreach ($bundle->depends as $depend) { - $this->registerBundle($bundles, $depend, $registered); - } - unset($registered[$name]); - $registered[$name] = true; - } elseif ($registered[$name] === false) { - throw new Exception("A circular dependency is detected for target '$name'."); - } - } - - protected function saveTargets($targets, $bundleFile) - { - $array = array(); - foreach ($targets as $name => $target) { - foreach (array('js', 'css', 'depends', 'basePath', 'baseUrl') as $prop) { - if (!empty($target->$prop)) { - $array[$name][$prop] = $target->$prop; - } - } - } - $array = var_export($array, true); - $version = date('Y-m-d H:i:s', time()); - file_put_contents($bundleFile, << Date: Mon, 22 Apr 2013 19:45:37 -0400 Subject: [PATCH 096/104] Added a basic app. --- app/assets/.gitignore | 1 + app/index.php | 9 ++++++ app/protected/config/main.php | 15 ++++++++++ app/protected/controllers/SiteController.php | 22 ++++++++++++++ app/protected/models/User.php | 43 ++++++++++++++++++++++++++++ app/protected/runtime/.gitignore | 1 + app/protected/views/layouts/main.php | 22 ++++++++++++++ app/protected/views/site/index.php | 17 +++++++++++ framework/web/UrlManager.php | 8 +++--- 9 files changed, 134 insertions(+), 4 deletions(-) create mode 100644 app/assets/.gitignore create mode 100644 app/index.php create mode 100644 app/protected/config/main.php create mode 100644 app/protected/controllers/SiteController.php create mode 100644 app/protected/models/User.php create mode 100644 app/protected/runtime/.gitignore create mode 100644 app/protected/views/layouts/main.php create mode 100644 app/protected/views/site/index.php diff --git a/app/assets/.gitignore b/app/assets/.gitignore new file mode 100644 index 0000000..72e8ffc --- /dev/null +++ b/app/assets/.gitignore @@ -0,0 +1 @@ +* diff --git a/app/index.php b/app/index.php new file mode 100644 index 0000000..8f98090 --- /dev/null +++ b/app/index.php @@ -0,0 +1,9 @@ +run(); diff --git a/app/protected/config/main.php b/app/protected/config/main.php new file mode 100644 index 0000000..e18ead8 --- /dev/null +++ b/app/protected/config/main.php @@ -0,0 +1,15 @@ + 'hello', + 'basePath' => dirname(__DIR__), + 'components' => array( + 'cache' => array( + 'class' => 'yii\caching\FileCache', + ), + 'user' => array( + 'class' => 'yii\web\User', + 'identityClass' => 'app\models\User', + ) + ), +); \ No newline at end of file diff --git a/app/protected/controllers/SiteController.php b/app/protected/controllers/SiteController.php new file mode 100644 index 0000000..58e9568 --- /dev/null +++ b/app/protected/controllers/SiteController.php @@ -0,0 +1,22 @@ +render('index'); + } + + public function actionLogin() + { + $user = app\models\User::findIdentity(100); + Yii::$app->getUser()->login($user); + Yii::$app->getResponse()->redirect(array('site/index')); + } + + public function actionLogout() + { + Yii::$app->getUser()->logout(); + Yii::$app->getResponse()->redirect(array('site/index')); + } +} \ No newline at end of file diff --git a/app/protected/models/User.php b/app/protected/models/User.php new file mode 100644 index 0000000..cebf1da --- /dev/null +++ b/app/protected/models/User.php @@ -0,0 +1,43 @@ + array( + 'id' => '100', + 'authKey' => 'test100key', + 'name' => 'admin', + ), + '101' => array( + 'id' => '101', + 'authKey' => 'test101key', + 'name' => 'demo', + ), + ); + + public static function findIdentity($id) + { + return isset(self::$users[$id]) ? new self(self::$users[$id]) : null; + } + + public function getId() + { + return $this->id; + } + + public function getAuthKey() + { + return $this->authKey; + } + + public function validateAuthKey($authKey) + { + return $this->authKey === $authKey; + } +} \ No newline at end of file diff --git a/app/protected/runtime/.gitignore b/app/protected/runtime/.gitignore new file mode 100644 index 0000000..72e8ffc --- /dev/null +++ b/app/protected/runtime/.gitignore @@ -0,0 +1 @@ +* diff --git a/app/protected/views/layouts/main.php b/app/protected/views/layouts/main.php new file mode 100644 index 0000000..092e665 --- /dev/null +++ b/app/protected/views/layouts/main.php @@ -0,0 +1,22 @@ + + + +beginPage(); ?> + + <?php echo Html::encode($this->title); ?> + head(); ?> + + +

    Welcome

    +beginBody(); ?> + +endBody(); ?> + +endPage(); ?> + diff --git a/app/protected/views/site/index.php b/app/protected/views/site/index.php new file mode 100644 index 0000000..3b83080 --- /dev/null +++ b/app/protected/views/site/index.php @@ -0,0 +1,17 @@ +title = 'Hello World'; + +$user = Yii::$app->getUser(); +if ($user->isGuest) { + echo Html::a('login', array('login')); +} else { + echo "You are logged in as " . $user->identity->name . "
    "; + echo Html::a('logout', array('logout')); +} +?> + + diff --git a/framework/web/UrlManager.php b/framework/web/UrlManager.php index 459e8e8..755d644 100644 --- a/framework/web/UrlManager.php +++ b/framework/web/UrlManager.php @@ -74,9 +74,6 @@ class UrlManager extends Component public function init() { parent::init(); - if (is_string($this->cache)) { - $this->cache = Yii::$app->getComponent($this->cache); - } $this->compileRules(); } @@ -88,6 +85,9 @@ class UrlManager extends Component if (!$this->enablePrettyUrl || $this->rules === array()) { return; } + if (is_string($this->cache)) { + $this->cache = Yii::$app->getComponent($this->cache); + } if ($this->cache instanceof Cache) { $key = $this->cache->buildKey(__CLASS__); $hash = md5(json_encode($this->rules)); @@ -104,7 +104,7 @@ class UrlManager extends Component $this->rules[$i] = Yii::createObject($rule); } - if ($this->cache instanceof Cache) { + if (isset($key, $hash)) { $this->cache->set($key, array($this->rules, $hash)); } } From 173706f5165a7c28717c9203b7ea1868991b7889 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Tue, 23 Apr 2013 14:11:40 +0400 Subject: [PATCH 097/104] updated expected exception message --- tests/unit/framework/base/ModelTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/framework/base/ModelTest.php b/tests/unit/framework/base/ModelTest.php index aa15230..f04e550 100644 --- a/tests/unit/framework/base/ModelTest.php +++ b/tests/unit/framework/base/ModelTest.php @@ -195,7 +195,7 @@ class ModelTest extends TestCase public function testCreateValidators() { - $this->setExpectedException('yii\base\InvalidConfigException', 'Invalid validation rule: a rule must be an array specifying both attribute names and validator type.'); + $this->setExpectedException('yii\base\InvalidConfigException', 'Invalid validation rule: a rule must specify both attribute names and validator type.'); $invalid = new InvalidRulesModel(); $invalid->createValidators(); From a9215ceddd74b9963784612d7c1e1b74fdd19b2d Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Tue, 23 Apr 2013 14:11:55 +0400 Subject: [PATCH 098/104] create a webapp in test bootstrap --- tests/unit/bootstrap.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/unit/bootstrap.php b/tests/unit/bootstrap.php index 4a388c6..8290bbe 100644 --- a/tests/unit/bootstrap.php +++ b/tests/unit/bootstrap.php @@ -9,4 +9,6 @@ require_once(__DIR__ . '/../../framework/yii.php'); Yii::setAlias('@yiiunit', __DIR__); +new \yii\web\Application(array('id' => 'testapp', 'basePath' => __DIR__)); + require_once(__DIR__ . '/TestCase.php'); From 3e2e4afa8560074797fce9cafc27577d13b78e57 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Tue, 23 Apr 2013 14:15:22 +0400 Subject: [PATCH 099/104] fixed DB quoting typo --- framework/db/Connection.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework/db/Connection.php b/framework/db/Connection.php index 695034a..797508a 100644 --- a/framework/db/Connection.php +++ b/framework/db/Connection.php @@ -522,7 +522,7 @@ class Connection extends Component if (isset($matches[3])) { return $db->quoteColumnName($matches[3]); } else { - return str_replace('%', $this->tablePrefix, $db->quoteTableName($matches[2])); + return str_replace('%', $db->tablePrefix, $db->quoteTableName($matches[2])); } }, $sql); } From 08be696434a2fbdf61f3a309fffc73c5e80785f2 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Tue, 23 Apr 2013 14:32:54 +0400 Subject: [PATCH 100/104] fixed Html test under Windows (line endings) --- tests/unit/framework/helpers/HtmlTest.php | 46 ++++++++++++++++++------------- 1 file changed, 27 insertions(+), 19 deletions(-) diff --git a/tests/unit/framework/helpers/HtmlTest.php b/tests/unit/framework/helpers/HtmlTest.php index bf0ca0a..4077043 100644 --- a/tests/unit/framework/helpers/HtmlTest.php +++ b/tests/unit/framework/helpers/HtmlTest.php @@ -22,6 +22,14 @@ class HtmlTest extends \yii\test\TestCase )); } + public function assertEqualsWithoutLE($expected, $actual) + { + $expected = str_replace("\r\n", "\n", $expected); + $actual = str_replace("\r\n", "\n", $actual); + + $this->assertEquals($expected, $actual); + } + public function tearDown() { Yii::$app = null; @@ -240,21 +248,21 @@ class HtmlTest extends \yii\test\TestCase EOD; - $this->assertEquals($expected, Html::dropDownList('test')); + $this->assertEqualsWithoutLE($expected, Html::dropDownList('test')); $expected = << EOD; - $this->assertEquals($expected, Html::dropDownList('test', null, $this->getDataItems())); + $this->assertEqualsWithoutLE($expected, Html::dropDownList('test', null, $this->getDataItems())); $expected = << EOD; - $this->assertEquals($expected, Html::dropDownList('test', 'value2', $this->getDataItems())); + $this->assertEqualsWithoutLE($expected, Html::dropDownList('test', 'value2', $this->getDataItems())); } public function testListBox() @@ -264,48 +272,48 @@ EOD; EOD; - $this->assertEquals($expected, Html::listBox('test')); + $this->assertEqualsWithoutLE($expected, Html::listBox('test')); $expected = << EOD; - $this->assertEquals($expected, Html::listBox('test', null, $this->getDataItems(), array('size' => 5))); + $this->assertEqualsWithoutLE($expected, Html::listBox('test', null, $this->getDataItems(), array('size' => 5))); $expected = << EOD; - $this->assertEquals($expected, Html::listBox('test', null, $this->getDataItems2())); + $this->assertEqualsWithoutLE($expected, Html::listBox('test', null, $this->getDataItems2())); $expected = << EOD; - $this->assertEquals($expected, Html::listBox('test', 'value2', $this->getDataItems())); + $this->assertEqualsWithoutLE($expected, Html::listBox('test', 'value2', $this->getDataItems())); $expected = << EOD; - $this->assertEquals($expected, Html::listBox('test', array('value1', 'value2'), $this->getDataItems())); + $this->assertEqualsWithoutLE($expected, Html::listBox('test', array('value1', 'value2'), $this->getDataItems())); $expected = << EOD; - $this->assertEquals($expected, Html::listBox('test', null, array(), array('multiple' => true))); + $this->assertEqualsWithoutLE($expected, Html::listBox('test', null, array(), array('multiple' => true))); $expected = << EOD; - $this->assertEquals($expected, Html::listBox('test', '', array(), array('unselect' => '0'))); + $this->assertEqualsWithoutLE($expected, Html::listBox('test', '', array(), array('unselect' => '0'))); } public function testCheckboxList() @@ -316,19 +324,19 @@ EOD; EOD; - $this->assertEquals($expected, Html::checkboxList('test', array('value2'), $this->getDataItems())); + $this->assertEqualsWithoutLE($expected, Html::checkboxList('test', array('value2'), $this->getDataItems())); $expected = << text1<> EOD; - $this->assertEquals($expected, Html::checkboxList('test', array('value2'), $this->getDataItems2())); + $this->assertEqualsWithoutLE($expected, Html::checkboxList('test', array('value2'), $this->getDataItems2())); $expected = <<
    EOD; - $this->assertEquals($expected, Html::checkboxList('test', array('value2'), $this->getDataItems(), array( + $this->assertEqualsWithoutLE($expected, Html::checkboxList('test', array('value2'), $this->getDataItems(), array( 'separator' => "
    \n", 'unselect' => '0', ))); @@ -337,7 +345,7 @@ EOD; 0 1 EOD; - $this->assertEquals($expected, Html::checkboxList('test', array('value2'), $this->getDataItems(), array( + $this->assertEqualsWithoutLE($expected, Html::checkboxList('test', array('value2'), $this->getDataItems(), array( 'item' => function ($index, $label, $name, $checked, $value) { return $index . Html::label($label . ' ' . Html::checkbox($name, $checked, $value)); } @@ -352,19 +360,19 @@ EOD; EOD; - $this->assertEquals($expected, Html::radioList('test', array('value2'), $this->getDataItems())); + $this->assertEqualsWithoutLE($expected, Html::radioList('test', array('value2'), $this->getDataItems())); $expected = << text1<> EOD; - $this->assertEquals($expected, Html::radioList('test', array('value2'), $this->getDataItems2())); + $this->assertEqualsWithoutLE($expected, Html::radioList('test', array('value2'), $this->getDataItems2())); $expected = <<
    EOD; - $this->assertEquals($expected, Html::radioList('test', array('value2'), $this->getDataItems(), array( + $this->assertEqualsWithoutLE($expected, Html::radioList('test', array('value2'), $this->getDataItems(), array( 'separator' => "
    \n", 'unselect' => '0', ))); @@ -373,7 +381,7 @@ EOD; 0 1 EOD; - $this->assertEquals($expected, Html::radioList('test', array('value2'), $this->getDataItems(), array( + $this->assertEqualsWithoutLE($expected, Html::radioList('test', array('value2'), $this->getDataItems(), array( 'item' => function ($index, $label, $name, $checked, $value) { return $index . Html::label($label . ' ' . Html::radio($name, $checked, $value)); } @@ -420,7 +428,7 @@ EOD; 'group12' => array('class' => 'group'), ), ); - $this->assertEquals($expected, Html::renderSelectOptions(array('value111', 'value1'), $data, $attributes)); + $this->assertEqualsWithoutLE($expected, Html::renderSelectOptions(array('value111', 'value1'), $data, $attributes)); } public function testRenderAttributes() From b0bf8f74068e867e1273d862e286ac7241cfb798 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Tue, 23 Apr 2013 14:40:52 +0400 Subject: [PATCH 101/104] fixed dbcache multiget --- framework/caching/DbCache.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework/caching/DbCache.php b/framework/caching/DbCache.php index dee8c7a..befb523 100644 --- a/framework/caching/DbCache.php +++ b/framework/caching/DbCache.php @@ -124,7 +124,7 @@ class DbCache extends Cache $query = new Query; $query->select(array('id', 'data')) ->from($this->cacheTable) - ->where(array('id' => $keys)) + ->where(array('in', 'id', (array)$keys)) ->andWhere('([[expire]] = 0 OR [[expire]] > ' . time() . ')'); if ($this->db->enableQueryCache) { From 09dbaeb70094626067578e82c3071c14bdb0e8cb Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Tue, 23 Apr 2013 15:00:41 +0400 Subject: [PATCH 102/104] more assertions for cache test --- tests/unit/framework/caching/CacheTest.php | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/unit/framework/caching/CacheTest.php b/tests/unit/framework/caching/CacheTest.php index ad2fcf5..f9db4f4 100644 --- a/tests/unit/framework/caching/CacheTest.php +++ b/tests/unit/framework/caching/CacheTest.php @@ -16,9 +16,9 @@ abstract class CacheTest extends TestCase public function testSet() { $cache = $this->getCacheInstance(); - $cache->set('string_test', 'string_test'); - $cache->set('number_test', 42); - $cache->set('array_test', array('array_test' => 'array_test')); + $this->assertTrue($cache->set('string_test', 'string_test')); + $this->assertTrue($cache->set('number_test', 42)); + $this->assertTrue($cache->set('array_test', array('array_test' => 'array_test'))); $cache['arrayaccess_test'] = new \stdClass(); } @@ -45,7 +45,7 @@ abstract class CacheTest extends TestCase public function testExpire() { $cache = $this->getCacheInstance(); - $cache->set('expire_test', 'expire_test', 2); + $this->assertTrue($cache->set('expire_test', 'expire_test', 2)); sleep(1); $this->assertEquals('expire_test', $cache->get('expire_test')); sleep(2); @@ -57,11 +57,11 @@ abstract class CacheTest extends TestCase $cache = $this->getCacheInstance(); // should not change existing keys - $cache->add('number_test', 13); + $this->assertFalse($cache->add('number_test', 13)); $this->assertEquals(42, $cache->get('number_test')); // should store data is it's not there yet - $cache->add('add_test', 13); + $this->assertTrue($cache->add('add_test', 13)); $this->assertEquals(13, $cache->get('add_test')); } @@ -69,14 +69,14 @@ abstract class CacheTest extends TestCase { $cache = $this->getCacheInstance(); - $cache->delete('number_test'); + $this->assertTrue($cache->delete('number_test')); $this->assertEquals(null, $cache->get('number_test')); } public function testFlush() { $cache = $this->getCacheInstance(); - $cache->flush(); + $this->assertTrue($cache->flush()); $this->assertEquals(null, $cache->get('add_test')); } } From c09ace8e96dc3e977e53d8ec105aa05ba0bb8ec3 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Tue, 23 Apr 2013 15:10:18 +0400 Subject: [PATCH 103/104] added note about enabling APC cache for CLI --- framework/caching/ApcCache.php | 1 + 1 file changed, 1 insertion(+) diff --git a/framework/caching/ApcCache.php b/framework/caching/ApcCache.php index dd954cc..391851d 100644 --- a/framework/caching/ApcCache.php +++ b/framework/caching/ApcCache.php @@ -11,6 +11,7 @@ namespace yii\caching; * ApcCache provides APC caching in terms of an application component. * * To use this application component, the [APC PHP extension](http://www.php.net/apc) must be loaded. + * In order to enable APC for CLI you should add "apc.enable_cli = 1" to your php.ini. * * See [[Cache]] for common cache operations that ApcCache supports. * From 15b9f97ca41ce218a13b26fa5bbebaf64708dcdb Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Tue, 23 Apr 2013 16:48:14 -0400 Subject: [PATCH 104/104] Fixed typo in query builder and reverted changes to DbCache.php --- framework/caching/DbCache.php | 2 +- framework/db/QueryBuilder.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/framework/caching/DbCache.php b/framework/caching/DbCache.php index befb523..dee8c7a 100644 --- a/framework/caching/DbCache.php +++ b/framework/caching/DbCache.php @@ -124,7 +124,7 @@ class DbCache extends Cache $query = new Query; $query->select(array('id', 'data')) ->from($this->cacheTable) - ->where(array('in', 'id', (array)$keys)) + ->where(array('id' => $keys)) ->andWhere('([[expire]] = 0 OR [[expire]] > ' . time() . ')'); if ($this->db->enableQueryCache) { diff --git a/framework/db/QueryBuilder.php b/framework/db/QueryBuilder.php index da43940..441d287 100644 --- a/framework/db/QueryBuilder.php +++ b/framework/db/QueryBuilder.php @@ -744,7 +744,7 @@ class QueryBuilder extends \yii\base\Object $parts = array(); foreach ($condition as $column => $value) { if (is_array($value)) { // IN condition - $parts[] = $this->buildInCondition('in', array($column, $value), $query); + $parts[] = $this->buildInCondition('in', array($column, $value), $params); } else { if (strpos($column, '(') === false) { $column = $this->db->quoteColumnName($column);